diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 589f103cb..d816aa2c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -577,7 +577,7 @@ jobs: runs-on: ubuntu-latest container: image: ubuntu:20.04 - timeout-minutes: 15 + timeout-minutes: 30 steps: - name: Checkout repository uses: actions/checkout@v4 @@ -589,7 +589,10 @@ jobs: libsqlite3-dev libssl-dev nlohmann-json3-dev libyaml-cpp-dev - name: Build the ROS-free cores - run: CXX=g++-9 cmake -S scripts/check_cores_without_ros -B /tmp/b && cmake --build /tmp/b + run: CXX=g++-9 cmake -S scripts/check_cores_without_ros -B /tmp/b && cmake --build /tmp/b --parallel "$(nproc)" + + - name: Build the core in hosts with their own httplib, at C++14 + run: CXX=g++-9 scripts/check_cores_without_ros/hosts/run.sh - name: Run the check program and verify it links no ROS library run: | diff --git a/scripts/check_cores_without_ros/CMakeLists.txt b/scripts/check_cores_without_ros/CMakeLists.txt index 68f440677..68c5f26df 100644 --- a/scripts/check_cores_without_ros/CMakeLists.txt +++ b/scripts/check_cores_without_ros/CMakeLists.txt @@ -20,6 +20,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) include(${CMAKE_CURRENT_LIST_DIR}/../../src/ros2_medkit_fault_reporter/cmake/fault_reporter_core.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../../src/ros2_medkit_fault_manager/cmake/fault_manager_core.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/../../src/ros2_medkit_gateway/cmake/gateway_core.cmake) add_executable(check_cores_without_ros main.cpp) -target_link_libraries(check_cores_without_ros PRIVATE fault_reporter_core fault_manager_core) +target_link_libraries(check_cores_without_ros PRIVATE fault_reporter_core fault_manager_core gateway_core) diff --git a/scripts/check_cores_without_ros/Dockerfile b/scripts/check_cores_without_ros/Dockerfile index 8d1288c3e..86f9b338e 100644 --- a/scripts/check_cores_without_ros/Dockerfile +++ b/scripts/check_cores_without_ros/Dockerfile @@ -5,7 +5,9 @@ WORKDIR /src COPY src/ros2_medkit_fault_detection src/ros2_medkit_fault_detection COPY src/ros2_medkit_fault_reporter src/ros2_medkit_fault_reporter COPY src/ros2_medkit_fault_manager src/ros2_medkit_fault_manager +COPY src/ros2_medkit_gateway src/ros2_medkit_gateway COPY scripts/check_cores_without_ros scripts/check_cores_without_ros -RUN CXX=g++-9 cmake -S scripts/check_cores_without_ros -B /tmp/b && cmake --build /tmp/b +RUN CXX=g++-9 cmake -S scripts/check_cores_without_ros -B /tmp/b && cmake --build /tmp/b --parallel "$(nproc)" +RUN CXX=g++-9 scripts/check_cores_without_ros/hosts/run.sh RUN ldd /tmp/b/check_cores_without_ros | tee /tmp/ldd.txt && ! grep -q rclcpp /tmp/ldd.txt CMD ["sh", "-c", "ldd /tmp/b/check_cores_without_ros; /tmp/b/check_cores_without_ros"] diff --git a/scripts/check_cores_without_ros/README.md b/scripts/check_cores_without_ros/README.md index 7665f5d89..2469f83e3 100644 --- a/scripts/check_cores_without_ros/README.md +++ b/scripts/check_cores_without_ros/README.md @@ -1,19 +1,24 @@ # check_cores_without_ros -Checks that the ROS-free cores (fault reporter core, fault manager core) build and run -without ROS. It builds them with plain CMake and gcc 9 on Ubuntu 20.04, the oldest compiler -the cores are checked with, then runs a small program that links them. The check fails if -the binary links `rclcpp`. +Checks that the ROS-free cores (fault reporter core, fault manager core, gateway core) build and +run without ROS. It builds them with plain CMake and gcc 9 on Ubuntu 20.04, the oldest compiler +the cores are checked with, then runs a small program that links them. The program also starts +the gateway REST server over minimal `GatewayServices` and reads `/api/v1/health`, `/api/v1/` and `/api/v1/version-info`. The check +fails if the binary links `rclcpp`. ```bash scripts/check_cores_without_ros/run.sh ``` -The CMake file includes `cmake/fault_reporter_core.cmake` and `cmake/fault_manager_core.cmake` by path, as the ament builds do. The image installs only g++-9, cmake, make and the dev packages of SQLite3, OpenSSL, nlohmann_json and -yaml-cpp. libpqxx is not installed, so the PostgreSQL backend is not built. +The CMake file includes `cmake/fault_reporter_core.cmake`, `cmake/fault_manager_core.cmake` and +`cmake/gateway_core.cmake` by path, as the ament builds do. The image installs only g++-9, +cmake, make and the dev packages of SQLite3, OpenSSL, nlohmann_json and yaml-cpp. libpqxx is +not installed, so the PostgreSQL backend is not built. ## What `main.cpp` checks +- Gateway: with `server_name`, `vendor_name` and `server_version` set in `RestConfig`, `GET /api/v1/` returns the name and version, and `GET /api/v1/version-info` returns the vendor name and version. + - Reporter core: a report below the local filter threshold is filtered, the next one reaches the sink, a PASSED report is filtered by the same threshold, an empty fault code is not forwarded. - Fault manager storage: a FAILED report creates a CONFIRMED fault, and `list_faults` returns it with its nanosecond time. @@ -26,6 +31,8 @@ yaml-cpp. libpqxx is not installed, so the PostgreSQL backend is not built. nothing, and the jobs then wait at most one period. - A `correlation.cleanup_interval_sec` of 9e9 does not overflow the due time. +`hosts/` holds two more plain-CMake hosts that build `gateway_core` with their own `httplib::httplib` target and compile their own code as C++14: one with a version 0.26 target whose include directory has no `httplib.h` (the vendored header must be used), one with an unversioned non-global target (CMake 3.16 cannot alias it). `hosts/run.sh` builds both and runs in the Dockerfile and in the CI job. + The Dockerfile also runs `ldd` on the binary and fails when `rclcpp` appears. The script sends only the needed source directories to `docker build` as the build diff --git a/scripts/check_cores_without_ros/hosts/CMakeLists.txt b/scripts/check_cores_without_ros/hosts/CMakeLists.txt new file mode 100644 index 000000000..36d210b25 --- /dev/null +++ b/scripts/check_cores_without_ros/hosts/CMakeLists.txt @@ -0,0 +1,42 @@ +# Copyright 2026 bburda +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# A plain-CMake host that differs from the main check: it supplies its own httplib::httplib, +# sets no global C++ standard and compiles its own code as C++14. +# HOST_HTTPLIB_VERSION httplib_VERSION to set; empty leaves it undefined +# HOST_HTTPLIB_INCLUDE include directory of the imported httplib::httplib +# HOST_HTTPLIB_GLOBAL ON makes the imported target GLOBAL +cmake_minimum_required(VERSION 3.16) +project(check_gateway_core_host LANGUAGES CXX) + +set(HOST_HTTPLIB_VERSION "" CACHE STRING "") +set(HOST_HTTPLIB_INCLUDE "" CACHE PATH "") +option(HOST_HTTPLIB_GLOBAL "" OFF) + +if(HOST_HTTPLIB_GLOBAL) + add_library(httplib::httplib INTERFACE IMPORTED GLOBAL) +else() + add_library(httplib::httplib INTERFACE IMPORTED) +endif() +set_target_properties(httplib::httplib PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${HOST_HTTPLIB_INCLUDE}") +if(NOT HOST_HTTPLIB_VERSION STREQUAL "") + set(httplib_VERSION "${HOST_HTTPLIB_VERSION}") +endif() + +include(${CMAKE_CURRENT_LIST_DIR}/../../../src/ros2_medkit_gateway/cmake/gateway_core.cmake) + +add_executable(check_gateway_core_host consumer.cpp) +target_compile_features(check_gateway_core_host PRIVATE cxx_std_14) +set_target_properties(check_gateway_core_host PROPERTIES CXX_STANDARD 14 CXX_EXTENSIONS OFF) +target_link_libraries(check_gateway_core_host PRIVATE gateway_core) diff --git a/scripts/check_cores_without_ros/hosts/consumer.cpp b/scripts/check_cores_without_ros/hosts/consumer.cpp new file mode 100644 index 000000000..d76c647c2 --- /dev/null +++ b/scripts/check_cores_without_ros/hosts/consumer.cpp @@ -0,0 +1,21 @@ +// Copyright 2026 bburda +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Compiled as C++14: the public headers must bring in the standard they need. +#include + +int main() { + std::optional value = std::string("ok"); + return value ? 0 : 1; +} diff --git a/scripts/check_cores_without_ros/hosts/run.sh b/scripts/check_cores_without_ros/hosts/run.sh new file mode 100755 index 000000000..36f70d46d --- /dev/null +++ b/scripts/check_cores_without_ros/hosts/run.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright 2026 bburda +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Builds gateway_core in two hosts that bring their own httplib::httplib (see CMakeLists.txt). +# Run from the repository root with CXX set to the compiler under test. +set -euo pipefail + +here="$(cd "$(dirname "$0")" && pwd)" +vendored="$here/../../../src/ros2_medkit_gateway/src/vendored/cpp_httplib" +empty="$(mktemp -d)" +trap 'rm -rf "$empty"' EXIT + +# Version 0.26 has no multipart API the handlers use: the target must be ignored. Its include +# directory holds no httplib.h, so using it fails the build. GLOBAL so that CMake 3.16 accepts it. +cmake -S "$here" -B /tmp/host_new -DHOST_HTTPLIB_VERSION=0.26.0 -DHOST_HTTPLIB_INCLUDE="$empty" -DHOST_HTTPLIB_GLOBAL=ON +cmake --build /tmp/host_new --parallel "$(nproc)" +/tmp/host_new/check_gateway_core_host + +# An unversioned target from find_package() is not GLOBAL: CMake 3.16 and 3.17 cannot alias it. +cmake -S "$here" -B /tmp/host_unversioned -DHOST_HTTPLIB_INCLUDE="$vendored" +cmake --build /tmp/host_unversioned --parallel "$(nproc)" +/tmp/host_unversioned/check_gateway_core_host +echo "Host variants OK." diff --git a/scripts/check_cores_without_ros/main.cpp b/scripts/check_cores_without_ros/main.cpp index dc698ef05..6a8cf8d90 100644 --- a/scripts/check_cores_without_ros/main.cpp +++ b/scripts/check_cores_without_ros/main.cpp @@ -12,19 +12,54 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include +#include +#include +#include + +#include #include #include #include #include +#include #include +#include #include +#ifndef CPPHTTPLIB_OPENSSL_SUPPORT +#error "gateway_core must export CPPHTTPLIB_OPENSSL_SUPPORT" +#endif +#include +#include + #include "ros2_medkit_fault_manager/core/fault_manager_config.hpp" #include "ros2_medkit_fault_manager/core/fault_manager_core.hpp" #include "ros2_medkit_fault_manager/core/fault_manager_startup.hpp" #include "ros2_medkit_fault_manager/core/log_sink.hpp" #include "ros2_medkit_fault_manager/sqlite_fault_storage.hpp" #include "ros2_medkit_fault_reporter/core/reporter_core.hpp" +#include "ros2_medkit_gateway/core/auth/auth_config.hpp" +#include "ros2_medkit_gateway/core/config.hpp" +#include "ros2_medkit_gateway/core/discovery/entity_discovery.hpp" +#include "ros2_medkit_gateway/core/gateway_services.hpp" +#include "ros2_medkit_gateway/core/http/rate_limiter.hpp" +#include "ros2_medkit_gateway/core/http/rest_server.hpp" +#include "ros2_medkit_gateway/core/http/sse_client_tracker.hpp" +#include "ros2_medkit_gateway/core/managers/configuration_manager.hpp" +#include "ros2_medkit_gateway/core/managers/data_access_manager.hpp" +#include "ros2_medkit_gateway/core/managers/fault_manager.hpp" +#include "ros2_medkit_gateway/core/managers/operation_manager.hpp" +#include "ros2_medkit_gateway/core/managers/subscription_manager.hpp" +#include "ros2_medkit_gateway/core/models/thread_safe_entity_cache.hpp" +#include "ros2_medkit_gateway/core/resource_sampler.hpp" +#include "ros2_medkit_gateway/core/subscription_transport.hpp" +#include "ros2_medkit_gateway/core/transports/action_transport.hpp" +#include "ros2_medkit_gateway/core/transports/fault_service_transport.hpp" +#include "ros2_medkit_gateway/core/transports/parameter_transport.hpp" +#include "ros2_medkit_gateway/core/transports/service_transport.hpp" +#include "ros2_medkit_gateway/core/transports/topic_transport.hpp" +#include "ros2_medkit_gateway/core/types/type_schema_provider.hpp" namespace core = ros2_medkit_fault_reporter::core; using ros2_medkit_fault_reporter::FilterConfig; @@ -74,6 +109,304 @@ class FixedWallClock : public ros2_medkit_fault_manager::core::WallClock { } }; +namespace gw = ros2_medkit_gateway; + +// Minimal transports: every call answers "not supported". +class NoFaultTransport : public gw::FaultServiceTransport { + public: + gw::FaultResult report_fault(const std::string & /*fault_code*/, uint8_t /*severity*/, + const std::string & /*description*/, const std::string & /*source_id*/) override { + return none(); + } + gw::FaultResult list_faults(const std::string & /*source_id*/, bool /*include_prefailed*/, bool /*include_confirmed*/, + bool /*include_cleared*/, bool /*include_healed*/, bool /*include_muted*/, + bool /*include_clusters*/) override { + return none(); + } + gw::FaultWithEnvJsonResult get_fault_with_env(const std::string & /*fault_code*/, + const std::string & /*source_id*/) override { + return {false, "not supported", {}, gw::FaultFailure::Unavailable}; + } + gw::FaultResult get_fault(const std::string & /*fault_code*/, const std::string & /*source_id*/) override { + return none(); + } + gw::FaultResult clear_fault(const std::string & /*fault_code*/, bool /*skip_correlation_auto_clear*/) override { + return none(); + } + gw::FaultResult get_snapshots(const std::string & /*fault_code*/, const std::string & /*topic*/) override { + return none(); + } + gw::FaultResult get_rosbag(const std::string & /*fault_code*/) override { + return none(); + } + gw::FaultResult list_rosbags(const std::string & /*entity_fqn*/) override { + return none(); + } + bool wait_for_services(std::chrono::duration /*timeout*/) override { + return false; + } + bool is_available() const override { + return false; + } + + private: + static gw::FaultResult none() { + return {false, {}, "not supported", gw::FaultFailure::Unavailable}; + } +}; + +class NoParameterTransport : public gw::ParameterTransport { + public: + bool is_self_node(const std::string & /*node_name*/) const override { + return false; + } + gw::ParameterResult list_parameters(const std::string & /*node_name*/) override { + return none(); + } + gw::ParameterResult get_parameter(const std::string & /*node_name*/, const std::string & /*param_name*/) override { + return none(); + } + gw::ParameterResult set_parameter(const std::string & /*node_name*/, const std::string & /*param_name*/, + const gw::json & /*value*/) override { + return none(); + } + gw::ParameterResult list_own_parameters() override { + return none(); + } + gw::ParameterResult get_own_parameter(const std::string & /*param_name*/) override { + return none(); + } + gw::ParameterResult get_default(const std::string & /*node_name*/, const std::string & /*param_name*/) override { + return none(); + } + gw::ParameterResult list_defaults(const std::string & /*node_name*/) override { + return none(); + } + bool is_node_available(const std::string & /*node_name*/) const override { + return false; + } + void shutdown() override { + } + + private: + static gw::ParameterResult none() { + return {false, {}, "not supported", gw::ParameterErrorCode::SERVICE_UNAVAILABLE}; + } +}; + +class NoTopicTransport : public gw::TopicTransport { + public: + gw::json publish(const std::string & /*topic_path*/, const std::string & /*msg_type*/, const gw::json & /*data*/, + std::chrono::duration /*timeout*/) override { + return {{"error", "not supported"}}; + } + gw::TopicSample sample(const std::string & topic, std::chrono::duration /*timeout*/) override { + gw::TopicSample out{}; + out.success = false; + out.status = "error"; + out.topic = topic; + out.error_message = "not supported"; + return out; + } + std::pair count_publishers_subscribers(const std::string & /*topic_name*/) const override { + return {0, 0}; + } +}; + +class NoServiceTransport : public gw::ServiceTransport { + public: + gw::ServiceCallResult call(const std::string & /*service_path*/, const std::string & /*service_type*/, + const gw::json & /*request*/, std::chrono::duration /*timeout*/) override { + gw::ServiceCallResult out{}; + out.success = false; + out.error_message = "not supported"; + return out; + } +}; + +class NoActionTransport : public gw::ActionTransport { + public: + gw::ActionSendGoalResult send_goal(const std::string & /*action_path*/, const std::string & /*action_type*/, + const gw::json & /*goal*/, std::chrono::duration /*timeout*/) override { + gw::ActionSendGoalResult out{}; + out.success = false; + out.goal_accepted = false; + out.error_message = "not supported"; + return out; + } + gw::ActionCancelResult cancel_goal(const std::string & /*action_path*/, const std::string & /*goal_id*/, + std::chrono::duration /*timeout*/) override { + gw::ActionCancelResult out; + out.error_message = "not supported"; + return out; + } + gw::ActionGetResultResult get_result(const std::string & /*action_path*/, const std::string & /*action_type*/, + const std::string & /*goal_id*/, + std::chrono::duration /*timeout*/) override { + return {false, gw::ActionGoalStatus::UNKNOWN, {}, "not supported"}; + } + void subscribe_status(const std::string & /*action_path*/, StatusCallback /*callback*/) override { + } + void unsubscribe_status(const std::string & /*action_path*/) override { + } +}; + +class EmptyDiscovery : public gw::EntityDiscovery { + public: + std::optional get_area(const std::string & /*id*/) override { + return std::nullopt; + } + std::optional get_component(const std::string & /*id*/) override { + return std::nullopt; + } + std::optional get_app(const std::string & /*id*/) override { + return std::nullopt; + } + std::optional get_function(const std::string & /*id*/) override { + return std::nullopt; + } + gw::DiscoveryMode get_mode() const override { + return gw::DiscoveryMode::RUNTIME_ONLY; + } + std::string get_strategy_name() const override { + return "none"; + } + std::optional get_merge_report() const override { + return std::nullopt; + } + std::optional get_linking_result() const override { + return std::nullopt; + } + gw::discovery::ManifestConfig get_manifest_config() const override { + return {}; + } +}; + +class NoTypeSchemas : public gw::TypeSchemaProvider { + public: + std::optional message_schema(const std::string & /*type_name*/) override { + return std::nullopt; + } + std::optional type_info(const std::string & /*type_name*/) override { + return std::nullopt; + } + std::shared_ptr service_type_info(const std::string & /*type_name*/) override { + return nullptr; + } + std::shared_ptr action_type_info(const std::string & /*type_name*/) override { + return nullptr; + } +}; + +// Every never-null getter is backed by a real core object; the optional ones stay null. +class MinimalServices : public gw::GatewayServices { + public: + MinimalServices() + : fault_(std::make_shared()) + , config_(std::make_shared()) + , data_(std::make_shared()) + , operations_(std::make_shared(), std::make_shared(), nullptr) + , sse_(std::make_shared(4)) { + } + const gw::ThreadSafeEntityCache & entity_cache() const override { + return cache_; + } + gw::EntityDiscovery * discovery() override { + return &discovery_; + } + gw::FaultManager * fault_manager() override { + return &fault_; + } + gw::ConfigurationManager * configuration_manager() override { + return &config_; + } + gw::DataAccessManager * data_access_manager() override { + return &data_; + } + gw::OperationManager * operation_manager() override { + return &operations_; + } + gw::SubscriptionManager * subscription_manager() override { + return &subscriptions_; + } + gw::ResourceSamplerRegistry * sampler_registry() override { + return &samplers_; + } + gw::TransportRegistry * transport_registry() override { + return &transports_; + } + gw::TypeSchemaProvider * type_schemas() override { + return &schemas_; + } + std::shared_ptr sse_client_tracker() override { + return sse_; + } + gw::GatewayNode * ros_node() override { + return nullptr; + } + gw::PluginManager * plugin_manager() override { + return nullptr; + } + gw::ScriptManager * script_manager() override { + return nullptr; + } + gw::LockManager * lock_manager() override { + return nullptr; + } + gw::UpdateManager * update_manager() override { + return nullptr; + } + gw::LogManager * log_manager() override { + return nullptr; + } + gw::BulkDataStore * bulk_data_store() override { + return nullptr; + } + gw::FaultTriggerEngine * fault_trigger_engine() override { + return nullptr; + } + gw::FreezeFrameSource * freeze_frames() override { + return nullptr; + } + gw::FaultEventSource * fault_events() override { + return nullptr; + } + gw::TopicDataProvider * topic_data_provider() override { + return nullptr; + } + gw::TriggerManager * trigger_manager() override { + return nullptr; + } + + private: + gw::ThreadSafeEntityCache cache_; + EmptyDiscovery discovery_; + NoTypeSchemas schemas_; + gw::FaultManager fault_; + gw::ConfigurationManager config_; + gw::DataAccessManager data_; + gw::OperationManager operations_; + gw::SubscriptionManager subscriptions_; + gw::ResourceSamplerRegistry samplers_; + gw::TransportRegistry transports_; + std::shared_ptr sse_; +}; + +int free_port() { + int fd = ::socket(AF_INET, SOCK_STREAM, 0); + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + if (fd < 0 || ::bind(fd, reinterpret_cast(&addr), sizeof(addr)) != 0) { + return 0; + } + socklen_t len = sizeof(addr); + ::getsockname(fd, reinterpret_cast(&addr), &len); + ::close(fd); + return ntohs(addr.sin_port); +} + bool check(bool ok, const char * what) { std::printf("%s: %s\n", ok ? "ok " : "FAIL", what); return ok; @@ -324,6 +657,58 @@ static int run() { std::filesystem::remove(corr_yaml); } + // Gateway: REST server over minimal services answers /health. + { + MinimalServices services; + const int port = free_port(); + ok &= check(port != 0, "an ephemeral port was found"); + gw::RestConfig rest; + rest.server_name = "Hosted Server"; + rest.vendor_name = "hosted_vendor"; + rest.server_version = "9.8.7"; + gw::RESTServer server(services, nullptr, "127.0.0.1", port, gw::CorsConfig{}, gw::AuthConfig{}, + gw::RateLimitConfig{}, rest); + std::thread server_thread([&server] { + server.start(); + }); + for (int i = 0; i < 100 && !server.is_running(); ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + httplib::Client client("127.0.0.1", port); + client.set_connection_timeout(5); + client.set_read_timeout(5); + auto res = client.Get("/api/v1/health"); + const int status = res ? res->status : 0; + ok &= check(status == 200, "GET /api/v1/health answers 200"); + if (status == 200) { + std::printf("ok gateway health %d\n", status); + } + auto root = client.Get("/api/v1/"); + ok &= check(root && root->status == 200, "GET /api/v1/ answers 200"); + if (root && root->status == 200) { + const auto body = nlohmann::json::parse(root->body, nullptr, false); + ok &= check(body.is_object() && body.value("name", "") == "Hosted Server", + "GET /api/v1/ names the server the host set"); + ok &= check(body.is_object() && body.value("version", "") == "9.8.7", + "GET /api/v1/ reports the server version the host set"); + } + auto info = client.Get("/api/v1/version-info"); + ok &= check(info && info->status == 200, "GET /api/v1/version-info answers 200"); + if (info && info->status == 200) { + const auto body = nlohmann::json::parse(info->body, nullptr, false); + const bool shaped = body.is_object() && body.contains("items") && body["items"].is_array() && + !body["items"].empty() && body["items"][0].contains("vendor_info"); + ok &= check(shaped, "GET /api/v1/version-info carries items[0].vendor_info"); + if (shaped) { + const auto & vendor = body["items"][0]["vendor_info"]; + ok &= check(vendor.value("name", "") == "hosted_vendor", "version-info vendor_info.name is the host's"); + ok &= check(vendor.value("version", "") == "9.8.7", "version-info vendor_info.version is the host's"); + } + } + server.stop(); + server_thread.join(); + } + return ok ? 0 : 1; } diff --git a/scripts/check_cores_without_ros/run.sh b/scripts/check_cores_without_ros/run.sh index 4312ec0b5..8b3ff54a1 100755 --- a/scripts/check_cores_without_ros/run.sh +++ b/scripts/check_cores_without_ros/run.sh @@ -18,7 +18,7 @@ trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM -tar -c src/ros2_medkit_fault_detection src/ros2_medkit_fault_reporter src/ros2_medkit_fault_manager scripts/check_cores_without_ros >"$ctx" || exit 1 +tar -c src/ros2_medkit_fault_detection src/ros2_medkit_fault_reporter src/ros2_medkit_fault_manager src/ros2_medkit_gateway scripts/check_cores_without_ros >"$ctx" || exit 1 image=$(docker build -q -f scripts/check_cores_without_ros/Dockerfile - <"$ctx") || exit 1 # Detached run: attached stdout can be cut short when the daemon is remote. container=$(docker run -d "$image") || exit 1 diff --git a/src/ros2_medkit_gateway/CMakeLists.txt b/src/ros2_medkit_gateway/CMakeLists.txt index 1dbddd18b..c969a0a03 100644 --- a/src/ros2_medkit_gateway/CMakeLists.txt +++ b/src/ros2_medkit_gateway/CMakeLists.txt @@ -14,14 +14,6 @@ include(ROS2MedkitCoverage) include(ROS2MedkitLinting) include(ROS2MedkitWarnings) -# Swagger UI embedding option -option(ENABLE_SWAGGER_UI "Embed Swagger UI assets in gateway binary" OFF) - -# Extract version from package.xml so version.hpp stays in sync automatically -file(READ "${CMAKE_CURRENT_SOURCE_DIR}/package.xml" _package_xml) -string(REGEX MATCH "([0-9]+\\.[0-9]+\\.[0-9]+)" _ "${_package_xml}") -set(GATEWAY_VERSION "${CMAKE_MATCH_1}") - # Find dependencies find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) @@ -57,31 +49,14 @@ add_compile_definitions(CPPHTTPLIB_OPENSSL_SUPPORT) # Emitted-status recorder (include/ros2_medkit_gateway/http/detail/status_recorder.hpp). # Instruments make_error() and every registry-mounted route so # test_openapi_error_coverage can assert the OpenAPI document declares every -# status the gateway actually serves. Directory scope and set before any -# target is created, because make_error() is an inline function shared by 13 -# translation units - a per-target definition would give it two bodies and -# break ODR. A shipped gateway compiles none of it: the Dockerfile builds with +# status the gateway actually serves. gateway_core carries the definition as a +# PUBLIC compile definition, so every translation unit sees one make_error(). +# A shipped gateway compiles none of it: the Dockerfile builds with # -DBUILD_TESTING=OFF. if(BUILD_TESTING) - add_compile_definitions(MEDKIT_STATUS_RECORDER) + set(GATEWAY_CORE_STATUS_RECORDER ON) endif() -# Vendored header-only libraries (no network access on ROS build farm) -# tl::expected v1.3.1 (CC0) - https://github.com/TartanLlama/expected -add_library(tl_expected_iface INTERFACE) -target_include_directories(tl_expected_iface SYSTEM INTERFACE - $ - $ -) -add_library(tl::expected ALIAS tl_expected_iface) - -# jwt-cpp v0.7.0 (MIT) - https://github.com/Thalhammer/jwt-cpp -add_library(jwt_cpp_iface INTERFACE) -target_include_directories(jwt_cpp_iface SYSTEM INTERFACE - ${CMAKE_CURRENT_SOURCE_DIR}/src/vendored/jwt_cpp/include -) -add_library(jwt-cpp::jwt-cpp ALIAS jwt_cpp_iface) - # mjansson/mdns (Public Domain) - https://github.com/mjansson/mdns # Header-only C library for mDNS/DNS-SD, used for peer gateway discovery include_directories(SYSTEM src/vendored/mdns) @@ -89,74 +64,17 @@ include_directories(SYSTEM src/vendored/mdns) # Include directories include_directories(include) -# ─── Neutral core static library ─────────────────────────────────────────── -# Middleware-neutral business logic. No ROS dependencies. Compiled from -# sources under src/core/ which were relocated from the flat src/ layout -# during the layered-architecture refactor. -file(GLOB_RECURSE GATEWAY_CORE_SOURCES - CONFIGURE_DEPENDS - "src/core/*.cpp" -) - -add_library(gateway_core STATIC ${GATEWAY_CORE_SOURCES}) - -target_include_directories(gateway_core - PUBLIC - $ - $ - PRIVATE - # Private implementation headers for the openapi module (not installed) - $ -) - -target_link_libraries(gateway_core - PUBLIC - cpp_httplib_target - nlohmann_json::nlohmann_json - yaml-cpp::yaml-cpp - OpenSSL::SSL - OpenSSL::Crypto - tl::expected - ros2_medkit_fault_detection::ros2_medkit_fault_detection - jwt-cpp::jwt-cpp - SQLite::SQLite3 - ${CMAKE_DL_LIBS} -) - -set_target_properties(gateway_core PROPERTIES - POSITION_INDEPENDENT_CODE ON - CXX_STANDARD 17 - CXX_STANDARD_REQUIRED ON - CXX_EXTENSIONS OFF -) - -# Pass version from package.xml to version.hpp -target_compile_definitions(gateway_core PUBLIC GATEWAY_VERSION_STRING="${GATEWAY_VERSION}") - -# Core PCH - heavy headers used across the neutral layer, no ROS includes. -# Speeds up full builds by ~30-40%. Disable with -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON. -target_precompile_headers(gateway_core PRIVATE - - - - - - - - - - - - - -) +# ─── ROS-free core static library ────────────────────────────────────────── +# Business logic, HTTP server, handlers, OpenAPI, plugin manager, aggregation and +# scripts. Defined once, in plain CMake. The ament build and +# scripts/check_cores_without_ros both include it. +include(cmake/gateway_core.cmake) # ─── ROS adapter static library ──────────────────────────────────────────── # Files that depend on rclcpp / rcl_interfaces / message-package interfaces. # Layered atop gateway_core via PUBLIC link so the executable and tests # transitively get both layers from a single dependency. add_library(gateway_ros2 STATIC - src/aggregation/aggregation_manager.cpp src/data/ros2_topic_data_provider.cpp src/ros2/conversions/fault_msg_conversions.cpp src/ros2/providers/ros2_runtime_introspection.cpp @@ -172,7 +90,6 @@ add_library(gateway_ros2 STATIC src/ros2/ros2_log_sink.cpp src/ros2/ros2_type_schema_provider.cpp src/ros2/trigger_topic_subscriber.cpp - src/default_script_provider.cpp src/discovery/discovery_manager.cpp src/discovery/layers/manifest_layer.cpp src/discovery/layers/plugin_layer.cpp @@ -184,41 +101,11 @@ add_library(gateway_ros2 STATIC src/entity_freeze_frame_capture.cpp src/fault_manager_paths.cpp src/gateway_node.cpp - src/http/fan_out_helpers.cpp - src/http/handlers/auth_handlers.cpp - src/http/handlers/bulkdata_handlers.cpp - src/http/handlers/config_handlers.cpp - src/http/handlers/cyclic_subscription_handlers.cpp - src/http/handlers/data_handlers.cpp - src/http/handlers/discovery_handlers.cpp - src/http/handlers/docs_handlers.cpp - src/http/handlers/fault_handlers.cpp - src/http/handlers/handler_context.cpp - src/http/handlers/health_handlers.cpp - src/http/handlers/lifecycle_handlers.cpp - src/http/handlers/lock_handlers.cpp - src/http/handlers/log_handlers.cpp - src/http/handlers/operation_handlers.cpp - src/http/handlers/script_handlers.cpp - src/http/handlers/sse_fault_handler.cpp - src/http/handlers/sse_transport_provider.cpp - src/http/handlers/trigger_handlers.cpp - src/http/handlers/update_handlers.cpp - src/http/http_server.cpp - src/http/rest_server.cpp - src/openapi/capability_generator.cpp - src/openapi/openapi_spec_builder.cpp - src/openapi/path_builder.cpp - src/openapi/schema_builder.cpp src/plugins/plugin_context.cpp - src/plugins/plugin_http_types.cpp - src/plugins/plugin_loader.cpp - src/plugins/plugin_manager.cpp src/ros2_common/callback_groups.cpp src/ros2_common/helper_node.cpp src/ros2_common/ros2_subscription_executor.cpp src/ros2_common/ros2_subscription_slot.cpp - src/script_manager.cpp src/trigger_fault_subscriber.cpp ) @@ -263,14 +150,6 @@ target_link_libraries(gateway_node gateway_ros2) # Export symbols to dlopen'd plugins (PluginContext::send_json, send_error, etc.) set_target_properties(gateway_node PROPERTIES ENABLE_EXPORTS ON) -# Apply Swagger UI compile definition and embed assets -if(ENABLE_SWAGGER_UI) - target_compile_definitions(gateway_ros2 PRIVATE ENABLE_SWAGGER_UI) - target_compile_definitions(gateway_node PRIVATE ENABLE_SWAGGER_UI) - include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/SwaggerUI.cmake) - target_compile_definitions(gateway_ros2 PRIVATE SWAGGER_UI_VERSION="${SWAGGER_UI_VERSION}") -endif() - # Install install(TARGETS gateway_node DESTINATION lib/${PROJECT_NAME} diff --git a/src/ros2_medkit_gateway/README.md b/src/ros2_medkit_gateway/README.md index 7e384d908..04125c74c 100644 --- a/src/ros2_medkit_gateway/README.md +++ b/src/ros2_medkit_gateway/README.md @@ -1749,8 +1749,17 @@ curl --cacert ./certs/ca.crt https://localhost:8443/api/v1 The package compiles into two layered static libraries with a strict dependency direction: -- **`gateway_core`** - middleware-neutral business logic. Sources live under `src/core/` and headers under `include/ros2_medkit_gateway/core/`. Links only header-only and C-level externals (cpp-httplib, nlohmann/json, yaml-cpp, tl::expected, jwt-cpp, OpenSSL, SQLite, dl). Carries no rclcpp / rcl_interfaces / message-package dependency. Hosts the neutral HTTP request handlers, JWT authentication, fault model (debounce, storage, cache, correlation), peer aggregation, manifest parsing, the entity cache, the neutral managers (lock, bulk-data, subscription, script, update, plugin), and every provider interface contract. -- **`gateway_ros2`** - ROS adapter layer. Compiles the remaining sources under `src/` and links `gateway_core` publicly via `medkit_target_dependencies`. Hosts `GatewayNode` (the `rclcpp::Node` entry point), the ROS-coupled managers (data access, operation, configuration, fault facade, log, trigger), runtime discovery, the native topic sampler, and the ROS-specific provider default implementations. +- **`gateway_core`** - the ROS-free layer, defined in `cmake/gateway_core.cmake`. Sources: `src/core/`, the HTTP server and REST server, `src/http/handlers/`, `src/openapi/`, the plugin manager and loader, aggregation, and the script manager. Headers live under `include/ros2_medkit_gateway/core/` and the neighbouring `http/`, `plugins/` and `aggregation/` directories. Links only header-only and C-level externals (cpp-httplib, nlohmann/json, yaml-cpp, tl::expected, jwt-cpp, OpenSSL, SQLite, dl). Carries no rclcpp / rcl_interfaces / message-package dependency. +- **`gateway_ros2`** - the ROS adapters. Compiles the remaining sources under `src/` and links `gateway_core` publicly via `medkit_target_dependencies`. Hosts `GatewayNode` (the `rclcpp::Node` entry point), the ROS transports and providers, runtime discovery, the native topic sampler, and the plugin context. + +The core compiles and is tested without ROS and without DDS setup. `cmake/gateway_core.cmake` is plain CMake. The ament build includes it by path, and so does `scripts/check_cores_without_ros`, which links `gateway_core`: + +```cmake +include(path/to/ros2_medkit_gateway/cmake/gateway_core.cmake) +target_link_libraries(check_cores_without_ros PRIVATE gateway_core) +``` + +The file takes a target the including build already defines, else `find_package`, else the copy vendored in `src/vendored/` (cpp-httplib, jwt-cpp, tl::expected). An existing `httplib::httplib` target is reused when it is older than 0.20 (or has no version), because 0.20 removed the multipart API the handlers use. `gateway_core` requires C++17 from its consumers (`cxx_std_17` is a public compile feature) and works with CMake 3.16. `-DENABLE_SWAGGER_UI=ON` works in both builds and needs network access and `xxd` at configure time. The check implements `GatewayServices` with minimal stand-ins and starts `RESTServer`. A program that hosts the core names its server through `RestConfig::server_name`, `vendor_name` and `server_version` (`name` and `version` on `GET /`, `vendor_info` on `GET /version-info`, and the OpenAPI title and version). The defaults are the ROS 2 node's values. `scripts/check_cores_without_ros/` builds and runs it on Ubuntu 20.04 with gcc 9, the oldest compiler the cores are checked with. The `gateway_node` executable and existing test targets link `gateway_ros2`, so they transitively get both layers from a single dependency. The neutral contract is enforced by two CTest checks: diff --git a/src/ros2_medkit_gateway/cmake/SwaggerUI.cmake b/src/ros2_medkit_gateway/cmake/SwaggerUI.cmake index 2f7b70216..9a4dcc024 100644 --- a/src/ros2_medkit_gateway/cmake/SwaggerUI.cmake +++ b/src/ros2_medkit_gateway/cmake/SwaggerUI.cmake @@ -16,8 +16,8 @@ # # When ENABLE_SWAGGER_UI=ON, downloads swagger-ui-dist files from unpkg.com # and generates a C++ source file with embedded byte arrays. -# The generated file is added to the gateway_ros2 target (the ROS adapter -# layer that owns the HTTP server and Swagger UI integration). +# The generated file is added to the gateway_core target (the layer that owns +# the HTTP server and Swagger UI integration). set(SWAGGER_UI_VERSION "5.17.14") set(SWAGGER_UI_URL "https://unpkg.com/swagger-ui-dist@${SWAGGER_UI_VERSION}") @@ -140,8 +140,8 @@ else() "or ensure network access to unpkg.com during CMake configure.") endif() -# Add generated source to gateway_ros2 and make the header findable -target_sources(gateway_ros2 PRIVATE "${SWAGGER_UI_ASSETS_CPP}") -target_include_directories(gateway_ros2 PRIVATE - "${CMAKE_CURRENT_SOURCE_DIR}/src/http" +# Add generated source to gateway_core and make the header findable +target_sources(gateway_core PRIVATE "${SWAGGER_UI_ASSETS_CPP}") +target_include_directories(gateway_core PRIVATE + "${_gwc_root}/src/http" ) diff --git a/src/ros2_medkit_gateway/cmake/gateway_core.cmake b/src/ros2_medkit_gateway/cmake/gateway_core.cmake new file mode 100644 index 000000000..65f8bdce7 --- /dev/null +++ b/src/ros2_medkit_gateway/cmake/gateway_core.cmake @@ -0,0 +1,170 @@ +# Copyright 2026 bburda +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ROS-free gateway layer: business logic, HTTP server, REST server, handlers, OpenAPI, +# plugin manager, aggregation and scripts. Plain CMake, no ament. Included by the ament +# build and by scripts/check_cores_without_ros. Defines the STATIC target `gateway_core`. +if(CMAKE_VERSION VERSION_LESS 3.16) + message(FATAL_ERROR "gateway_core needs CMake 3.16 or newer") +endif() + +set(_gwc_root "${CMAKE_CURRENT_LIST_DIR}/..") + +# Version string from package.xml, so every build reports the same one. +file(READ "${_gwc_root}/package.xml" _gwc_package_xml) +string(REGEX MATCH "([0-9]+\\.[0-9]+\\.[0-9]+)" _ "${_gwc_package_xml}") +set(_gwc_version "${CMAKE_MATCH_1}") + +find_package(Threads REQUIRED) + +# Targets from the including build win; otherwise find the library; otherwise use the vendored copy. +if(NOT TARGET SQLite::SQLite3) + find_package(SQLite3 REQUIRED) +endif() +if(NOT TARGET OpenSSL::SSL) + find_package(OpenSSL REQUIRED) +endif() +if(NOT TARGET nlohmann_json::nlohmann_json) + find_package(nlohmann_json REQUIRED) +endif() +# yaml-cpp 0.6 exports `yaml-cpp`, newer versions also `yaml-cpp::yaml-cpp`. +if(NOT TARGET yaml-cpp::yaml-cpp AND NOT TARGET yaml-cpp) + find_package(yaml-cpp REQUIRED) +endif() +if(TARGET yaml-cpp::yaml-cpp) + set(_gwc_yaml_cpp yaml-cpp::yaml-cpp) +else() + set(_gwc_yaml_cpp yaml-cpp) +endif() + +# A host-provided httplib::httplib is used only when it is older than 0.20: 0.20 removed the +# multipart API the handlers use. An unversioned target is trusted. +if(NOT TARGET cpp_httplib_target) + add_library(cpp_httplib_target INTERFACE) + if(TARGET httplib::httplib AND (NOT DEFINED httplib_VERSION OR httplib_VERSION VERSION_LESS "0.20")) + target_link_libraries(cpp_httplib_target INTERFACE httplib::httplib) + else() + target_include_directories(cpp_httplib_target SYSTEM INTERFACE "${_gwc_root}/src/vendored/cpp_httplib") + endif() + target_link_libraries(cpp_httplib_target INTERFACE Threads::Threads) +endif() +if(NOT TARGET tl::expected) + add_library(tl_expected_iface INTERFACE) + target_include_directories(tl_expected_iface SYSTEM INTERFACE + $) + add_library(tl::expected ALIAS tl_expected_iface) +endif() +if(NOT TARGET jwt-cpp::jwt-cpp) + add_library(jwt_cpp_iface INTERFACE) + target_include_directories(jwt_cpp_iface SYSTEM INTERFACE "${_gwc_root}/src/vendored/jwt_cpp/include") + add_library(jwt-cpp::jwt-cpp ALIAS jwt_cpp_iface) +endif() +if(TARGET ros2_medkit_fault_detection::ros2_medkit_fault_detection) + set(_gwc_fault_detection ros2_medkit_fault_detection::ros2_medkit_fault_detection) +else() + if(NOT TARGET ros2_medkit_fault_detection) + add_library(ros2_medkit_fault_detection INTERFACE) + target_include_directories(ros2_medkit_fault_detection INTERFACE + "${_gwc_root}/../ros2_medkit_fault_detection/include") + endif() + set(_gwc_fault_detection ros2_medkit_fault_detection) +endif() + +file(GLOB_RECURSE _gwc_core_sources CONFIGURE_DEPENDS "${_gwc_root}/src/core/*.cpp") +file(GLOB _gwc_handler_sources CONFIGURE_DEPENDS "${_gwc_root}/src/http/handlers/*.cpp") +file(GLOB _gwc_openapi_sources CONFIGURE_DEPENDS "${_gwc_root}/src/openapi/*.cpp") + +add_library(gateway_core STATIC + ${_gwc_core_sources} + ${_gwc_handler_sources} + ${_gwc_openapi_sources} + "${_gwc_root}/src/http/http_server.cpp" + "${_gwc_root}/src/http/rest_server.cpp" + "${_gwc_root}/src/http/fan_out_helpers.cpp" + "${_gwc_root}/src/plugins/plugin_manager.cpp" + "${_gwc_root}/src/plugins/plugin_loader.cpp" + "${_gwc_root}/src/plugins/plugin_http_types.cpp" + "${_gwc_root}/src/aggregation/aggregation_manager.cpp" + "${_gwc_root}/src/script_manager.cpp" + "${_gwc_root}/src/default_script_provider.cpp" +) + +target_include_directories(gateway_core + PUBLIC + $ + $ + PRIVATE + # Private implementation headers of the openapi module (not installed) + $ +) +target_include_directories(gateway_core SYSTEM PRIVATE "${_gwc_root}/src/vendored/mdns") + +target_link_libraries(gateway_core + PUBLIC + cpp_httplib_target + nlohmann_json::nlohmann_json + ${_gwc_yaml_cpp} + OpenSSL::SSL + OpenSSL::Crypto + tl::expected + ${_gwc_fault_detection} + jwt-cpp::jwt-cpp + SQLite::SQLite3 + Threads::Threads + ${CMAKE_DL_LIBS} +) + +# The public headers use C++17, so every consumer compiles as C++17 or newer. +target_compile_features(gateway_core PUBLIC cxx_std_17) + +set_target_properties(gateway_core PROPERTIES + POSITION_INDEPENDENT_CODE ON + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) + +target_compile_definitions(gateway_core PUBLIC + CPPHTTPLIB_OPENSSL_SUPPORT + GATEWAY_VERSION_STRING="${_gwc_version}") +# Emitted-status recorder (test builds only). Public: make_error() is inline and shared by every +# translation unit, so all of them must see the same definition. +if(GATEWAY_CORE_STATUS_RECORDER) + target_compile_definitions(gateway_core PUBLIC MEDKIT_STATUS_RECORDER) +endif() + +# Swagger UI: definitions and generated asset source, for every including build. +option(ENABLE_SWAGGER_UI "Embed Swagger UI assets in gateway binary" OFF) +if(ENABLE_SWAGGER_UI) + target_compile_definitions(gateway_core PUBLIC ENABLE_SWAGGER_UI) + include("${_gwc_root}/cmake/SwaggerUI.cmake") + target_compile_definitions(gateway_core PRIVATE SWAGGER_UI_VERSION="${SWAGGER_UI_VERSION}") +endif() + +# Heavy headers shared across the layer. Disable with -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON. +target_precompile_headers(gateway_core PRIVATE + + + + + + + + + + + + + +) diff --git a/src/ros2_medkit_gateway/design/index.rst b/src/ros2_medkit_gateway/design/index.rst index 5c841a91c..09c3e0799 100644 --- a/src/ros2_medkit_gateway/design/index.rst +++ b/src/ros2_medkit_gateway/design/index.rst @@ -12,23 +12,28 @@ Build Layers The package compiles into two layered static libraries with a strict dependency direction: -* ``gateway_core`` - middleware-neutral business logic. Sources live under - ``src/core/`` and headers under ``include/ros2_medkit_gateway/core/``. - Links only header-only and C-level externals (cpp-httplib, - nlohmann/json, yaml-cpp, tl::expected, jwt-cpp, OpenSSL, SQLite, dl). - Carries no rclcpp / rcl_interfaces / message-package dependency. - Hosts the neutral HTTP request handlers, JWT authentication, - fault model (debounce, storage, cache, correlation), peer aggregation, - manifest parsing, the entity cache, the neutral managers (lock, - bulk-data, subscription, script, update, plugin), and every provider - interface contract. -* ``gateway_ros2`` - ROS adapter layer. Compiles the remaining sources +* ``gateway_core`` - the ROS-free layer, defined in + ``cmake/gateway_core.cmake``. Sources: ``src/core/``, the HTTP server and + REST server, ``src/http/handlers/``, ``src/openapi/``, the plugin manager + and loader, aggregation, and the script manager. Links only header-only + and C-level externals (cpp-httplib, nlohmann/json, yaml-cpp, + tl::expected, jwt-cpp, OpenSSL, SQLite, dl). Carries no rclcpp / + rcl_interfaces / message-package dependency. +* ``gateway_ros2`` - the ROS adapters. Compiles the remaining sources under ``src/`` and links ``gateway_core`` publicly via ``medkit_target_dependencies``. Hosts ``GatewayNode`` (the - ``rclcpp::Node`` entry point), the ROS-coupled managers (data access, - operation, configuration, fault facade, log, trigger), runtime - discovery, the native topic sampler, and the ROS-specific provider - default implementations. + ``rclcpp::Node`` entry point), the ROS transports and providers, + runtime discovery, the native topic sampler, and the plugin context. + +``cmake/gateway_core.cmake`` is plain CMake. The ament build includes it by +path, and so does ``scripts/check_cores_without_ros``, which links +``gateway_core``. The file takes a target the including build already +defines, else ``find_package``, else the copy vendored in ``src/vendored/``. +The check implements ``GatewayServices`` with minimal stand-ins and starts +``RESTServer``. A program that hosts the core names its server through +``RestConfig::server_name``, ``vendor_name`` and ``server_version``; the +defaults are the ROS 2 node's values. ``scripts/check_cores_without_ros/`` builds and runs it on +Ubuntu 20.04 with gcc 9, the oldest compiler the cores are checked with. The ``gateway_node`` executable and existing test targets link ``gateway_ros2``, so they transitively get both layers from a single @@ -69,8 +74,8 @@ The following diagram shows the relationships between the main components of the note as N_layer_split This package compiles into two layered static libraries: - * gateway_core - middleware-neutral business logic (handlers, - auth, fault model, aggregation, all SOVD managers including + * gateway_core - the ROS-free layer (HTTP server, REST server, + handlers, OpenAPI, plugin manager, auth, fault model, aggregation, all SOVD managers including DataAccessManager, OperationManager, ConfigurationManager, FaultManager, LogManager, TriggerManager, providers, entity model). Does not link rclcpp. Managers consume neutral @@ -548,7 +553,7 @@ Each entry below is tagged with the static library it compiles into: - Automatically cleans up completed goals older than 5 minutes - JSON ↔ ROS 2 message conversion is performed inside the ROS-side transport adapter via ``ros2_medkit_serialization`` -4. **RESTServer** ``[gateway_ros2]`` - Provides the HTTP/REST API (route table couples to gateway lifecycle; the individual handlers it dispatches to live in ``gateway_core``) +4. **RESTServer** ``[gateway_core]`` - Provides the HTTP/REST API (route table couples to gateway lifecycle; the individual handlers it dispatches to live in ``gateway_core``) - Discovery endpoints: ``/health``, ``/areas``, ``/components`` - Data endpoints: ``/components/{id}/data``, ``/components/{id}/data/{topic}`` - Operations endpoints: ``/apps/{id}/operations``, ``/apps/{id}/operations/{op}/executions`` @@ -609,7 +614,7 @@ Each entry below is tagged with the static library it compiles into: - ``ActionInfo`` - Action metadata (path, name, type) - ``EntityCache`` - Thread-safe cache of discovered entities (areas, components, apps) -11. **ScriptManager** ``[gateway_ros2]`` - Manages diagnostic script upload, storage, and execution (SOVD 7.15) +11. **ScriptManager** ``[gateway_core]`` - Manages diagnostic script upload, storage, and execution (SOVD 7.15) - Delegates to a pluggable ``ScriptProvider`` backend (set via ``set_backend()``) - Lists, uploads, and deletes scripts per entity - Starts script executions as POSIX subprocesses with timeout support diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/config.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/config.hpp index 98f7aa1fd..9fbc5bdaa 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/config.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/config.hpp @@ -18,10 +18,23 @@ #include #include +#include "ros2_medkit_gateway/core/version.hpp" + namespace ros2_medkit_gateway { +/// How the server names itself to clients. The defaults describe the ROS 2 node. +struct ServerIdentity { + /// `name` on `GET /` and the title of the OpenAPI documents. + std::string server_name{"ROS 2 Medkit Gateway"}; + /// `vendor_info.name` on `GET /version-info`. + std::string vendor_name{"ros2_medkit"}; + /// `version` on `GET /`, `vendor_info.version` and the OpenAPI document version. + std::string server_version{kGatewayVersion}; +}; + /// Raw configuration values the REST layer reads. Clamping happens where the value is used. -struct RestConfig { +/// A program that hosts the core sets the `ServerIdentity` fields to its own values. +struct RestConfig : ServerIdentity { int64_t http_thread_pool_size{2}; int64_t keep_alive_timeout_sec{2}; int64_t sse_keepalive_interval_sec{30}; diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/gateway_services.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/gateway_services.hpp index 8334c5300..ce7e42b77 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/gateway_services.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/gateway_services.hpp @@ -67,11 +67,10 @@ class GatewayServices { virtual TransportRegistry * transport_registry() = 0; virtual TypeSchemaProvider * type_schemas() = 0; virtual std::shared_ptr sse_client_tracker() = 0; - /// Never null while SSEFaultHandler subscribes through it. Otherwise only - /// handed to subscription transports. - virtual GatewayNode * ros_node() = 0; // May be null. + /// Null when no ROS node exists. Only handed to subscription transports. + virtual GatewayNode * ros_node() = 0; virtual PluginManager * plugin_manager() = 0; virtual ScriptManager * script_manager() = 0; virtual LockManager * lock_manager() = 0; diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/docs_handlers.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/docs_handlers.hpp index 3db535cbf..2fd818f5b 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/docs_handlers.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/docs_handlers.hpp @@ -22,6 +22,7 @@ #include #include +#include "ros2_medkit_gateway/core/config.hpp" #include "ros2_medkit_gateway/http/handler_result.hpp" #include "ros2_medkit_gateway/http/handlers/handler_context.hpp" #include "ros2_medkit_gateway/http/typed_router.hpp" @@ -47,7 +48,7 @@ namespace handlers { class DocsHandlers { public: DocsHandlers(HandlerContext & ctx, PluginManager * plugin_mgr, const openapi::RouteRegistry * route_registry, - bool docs_enabled, std::string server_host, int64_t server_port); + bool docs_enabled, std::string server_host, int64_t server_port, ServerIdentity identity = {}); ~DocsHandlers(); /// GET /docs - Root capability description. @@ -87,6 +88,7 @@ class DocsHandlers { HandlerContext & ctx_; std::unique_ptr generator_; bool docs_enabled_{true}; + std::string server_name_; }; } // namespace handlers diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/health_handlers.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/health_handlers.hpp index 1a22d10c9..c8568897a 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/health_handlers.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/health_handlers.hpp @@ -17,6 +17,7 @@ #include #include +#include "ros2_medkit_gateway/core/config.hpp" #include "ros2_medkit_gateway/core/http/sse_client_tracker.hpp" #include "ros2_medkit_gateway/dto/health.hpp" @@ -54,9 +55,15 @@ class HealthHandlers { /// much of `sse.max_clients` is in use. Optional: a gateway built without /// one simply omits the field. /// @param docs_enabled Value of `docs.enabled`; hides the Swagger UI entry when false. + /// @param identity Name, vendor name and version reported on `GET /` and `GET /version-info`. explicit HealthHandlers(HandlerContext & ctx, const openapi::RouteRegistry * route_registry = nullptr, - std::shared_ptr sse_tracker = nullptr, bool docs_enabled = true) - : ctx_(ctx), route_registry_(route_registry), sse_tracker_(std::move(sse_tracker)), docs_enabled_(docs_enabled) { + std::shared_ptr sse_tracker = nullptr, bool docs_enabled = true, + ServerIdentity identity = {}) + : ctx_(ctx) + , route_registry_(route_registry) + , sse_tracker_(std::move(sse_tracker)) + , docs_enabled_(docs_enabled) + , identity_(std::move(identity)) { } /// GET /health - Health check endpoint @@ -73,6 +80,7 @@ class HealthHandlers { const openapi::RouteRegistry * route_registry_{nullptr}; std::shared_ptr sse_tracker_; bool docs_enabled_; + ServerIdentity identity_; }; } // namespace handlers diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/health.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/health.hpp index de01520d8..492e30a92 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/health.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/health.hpp @@ -209,7 +209,7 @@ inline constexpr std::string_view dto_name = "HealthStatus"; // // Wire shape (from health_handlers.cpp handle_version_info): // version - gateway version string (required) -// name - gateway name string, always "ros2_medkit" (required) +// name - vendor name the host configured, "ros2_medkit" by default (required) // ============================================================================= struct VersionInfoVendor { std::string version; @@ -390,7 +390,7 @@ inline constexpr std::string_view dto_name = "RootTls"; // RootOverview - response for GET / (API root). // // Wire shape (from health_handlers.cpp handle_root): -// name - "ROS 2 Medkit Gateway" (required) +// name - server name the host configured, "ROS 2 Medkit Gateway" by default (required) // version - gateway version string (required) // api_base - API base path string (required) // endpoints - array of endpoint description strings (required) diff --git a/src/ros2_medkit_gateway/scripts/check_core_purity.sh b/src/ros2_medkit_gateway/scripts/check_core_purity.sh index c930da6ef..9eab451a2 100755 --- a/src/ros2_medkit_gateway/scripts/check_core_purity.sh +++ b/src/ros2_medkit_gateway/scripts/check_core_purity.sh @@ -20,7 +20,22 @@ set -euo pipefail CORE_DIRS=( "include/ros2_medkit_gateway/core" + "include/ros2_medkit_gateway/http" + "include/ros2_medkit_gateway/plugins" + "include/ros2_medkit_gateway/aggregation" + "include/ros2_medkit_gateway/dto" "src/core" + "src/http" + "src/openapi" + "src/aggregation" +) +# Files of gateway_core outside those directories (cmake/gateway_core.cmake lists the same sources). +CORE_FILES=( + "src/plugins/plugin_manager.cpp" + "src/plugins/plugin_loader.cpp" + "src/plugins/plugin_http_types.cpp" + "src/script_manager.cpp" + "src/default_script_provider.cpp" ) # Denylist of ROS 2 include prefixes that must not appear in core/. @@ -48,32 +63,64 @@ for dir in "${CORE_DIRS[@]}"; do done < <(grep -rEn "$FORBIDDEN_PATTERN" "$dir" 2>/dev/null || true) done -# Files that must compile without any ROS include path. -ROS_FREE_SOURCES=( +for file in "${CORE_FILES[@]}"; do + while IFS= read -r match; do + echo "FORBIDDEN ROS INCLUDE: $match" + violations=$((violations + 1)) + done < <(grep -En "$FORBIDDEN_PATTERN" "$file" 2>/dev/null || true) +done + +# Every gateway_core source must compile without any ROS include path. +mapfile -t ROS_FREE_SOURCES < <({ + find src/core src/http src/openapi src/aggregation -name '*.cpp' + printf '%s\n' "${CORE_FILES[@]}" +} | sort) +ROS_FREE_HEADERS=( "include/ros2_medkit_gateway/core/gateway_services.hpp" "include/ros2_medkit_gateway/core/discovery/entity_discovery.hpp" "include/ros2_medkit_gateway/core/faults/freeze_frame_source.hpp" "include/ros2_medkit_gateway/core/faults/fault_event_source.hpp" "include/ros2_medkit_gateway/core/faults/fault_json.hpp" - "src/core/faults/fault_json.cpp" "include/ros2_medkit_gateway/core/discovery/linking_result.hpp" "include/ros2_medkit_gateway/core/types/type_schema_provider.hpp" "include/ros2_medkit_gateway/http/handlers/handler_context.hpp" - "src/http/handlers/handler_context.cpp" ) -for src in "${ROS_FREE_SOURCES[@]}"; do +# shellcheck disable=SC2317 +compile_check() { + local src="$1" if [ ! -f "$src" ]; then echo "ROS-FREE SOURCE MISSING: $src" - violations=$((violations + 1)) - continue + return 1 fi - if ! out=$(g++ -std=c++17 -fsyntax-only -Wno-pragma-once-outside-header \ - -Iinclude -I../ros2_medkit_fault_detection/include -Isrc -Isrc/vendored/tl_expected/include -Isrc/vendored/cpp_httplib "$src" 2>&1); then + local out + if ! out=$(g++ -std=c++17 -fsyntax-only -Wno-pragma-once-outside-header -DCPPHTTPLIB_OPENSSL_SUPPORT \ + -DGATEWAY_VERSION_STRING='"0"' \ + -Iinclude -I../ros2_medkit_fault_detection/include -Isrc -Isrc/openapi -Isrc/vendored/tl_expected/include \ + -Isrc/vendored/cpp_httplib -Isrc/vendored/jwt_cpp/include -Isrc/vendored/mdns "$src" 2>&1); then echo "DOES NOT COMPILE WITHOUT ROS: $src" echo "$out" | grep -E 'error' | head -5 - violations=$((violations + 1)) + return 1 fi -done +} +export -f compile_check +echo "Core purity: ${#ROS_FREE_SOURCES[@]} sources, ${#ROS_FREE_HEADERS[@]} headers compiled without ROS." +# Each compile_check prints one "CHECKED" line, so the verdict counts files compiled and does not +# depend on the pipeline status alone. +expected=$((${#ROS_FREE_SOURCES[@]} + ${#ROS_FREE_HEADERS[@]})) +results=$(mktemp) +trap 'rm -f "$results"' EXIT +xargs_status=0 +printf '%s\n' "${ROS_FREE_SOURCES[@]}" "${ROS_FREE_HEADERS[@]}" | + xargs -P "$(nproc)" -I{} bash -c 'compile_check {} && echo CHECKED || { echo CHECKED; echo FAILED; }' | + tee "$results" >&2 || xargs_status=$? +failed=$(grep -c '^FAILED' "$results" || true) +checked=$(grep -c '^CHECKED' "$results" || true) +violations=$((violations + failed)) +if [ "$xargs_status" -ne 0 ] || [ "$checked" -ne "$expected" ]; then + echo "Core purity check did not run: checked $checked of $expected files (xargs status $xargs_status)." + exit 1 +fi +echo "Core purity: $checked of $expected files compiled." if [ "$violations" -gt 0 ]; then echo "" diff --git a/src/ros2_medkit_gateway/src/core/discovery/manifest/manifest_config.cpp b/src/ros2_medkit_gateway/src/core/discovery/manifest/manifest_config.cpp new file mode 100644 index 000000000..3fae52302 --- /dev/null +++ b/src/ros2_medkit_gateway/src/core/discovery/manifest/manifest_config.cpp @@ -0,0 +1,51 @@ +// Copyright 2026 bburda +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "ros2_medkit_gateway/core/discovery/manifest/manifest.hpp" + +namespace ros2_medkit_gateway { +namespace discovery { + +// ManifestConfig helper implementations +ManifestConfig::UnmanifestedNodePolicy ManifestConfig::parse_policy(const std::string & str) { + if (str == "ignore") { + return UnmanifestedNodePolicy::IGNORE; + } + if (str == "error") { + return UnmanifestedNodePolicy::ERROR; + } + if (str == "include_as_orphan") { + return UnmanifestedNodePolicy::INCLUDE_AS_ORPHAN; + } + return UnmanifestedNodePolicy::WARN; // Default +} + +std::string ManifestConfig::policy_to_string(UnmanifestedNodePolicy policy) { + switch (policy) { + case UnmanifestedNodePolicy::IGNORE: + return "ignore"; + case UnmanifestedNodePolicy::ERROR: + return "error"; + case UnmanifestedNodePolicy::INCLUDE_AS_ORPHAN: + return "include_as_orphan"; + case UnmanifestedNodePolicy::WARN: + default: + return "warn"; + } +} + +} // namespace discovery +} // namespace ros2_medkit_gateway diff --git a/src/ros2_medkit_gateway/src/discovery/manifest/manifest_parser.cpp b/src/ros2_medkit_gateway/src/discovery/manifest/manifest_parser.cpp index 8202c8077..274679599 100644 --- a/src/ros2_medkit_gateway/src/discovery/manifest/manifest_parser.cpp +++ b/src/ros2_medkit_gateway/src/discovery/manifest/manifest_parser.cpp @@ -635,34 +635,6 @@ std::vector ManifestParser::get_string_vector(const YAML::Node & no return result; } -// ManifestConfig helper implementations -ManifestConfig::UnmanifestedNodePolicy ManifestConfig::parse_policy(const std::string & str) { - if (str == "ignore") { - return UnmanifestedNodePolicy::IGNORE; - } - if (str == "error") { - return UnmanifestedNodePolicy::ERROR; - } - if (str == "include_as_orphan") { - return UnmanifestedNodePolicy::INCLUDE_AS_ORPHAN; - } - return UnmanifestedNodePolicy::WARN; // Default -} - -std::string ManifestConfig::policy_to_string(UnmanifestedNodePolicy policy) { - switch (policy) { - case UnmanifestedNodePolicy::IGNORE: - return "ignore"; - case UnmanifestedNodePolicy::ERROR: - return "error"; - case UnmanifestedNodePolicy::INCLUDE_AS_ORPHAN: - return "include_as_orphan"; - case UnmanifestedNodePolicy::WARN: - default: - return "warn"; - } -} - ros2_medkit_gateway::ScriptEntryConfig ManifestParser::parse_script_entry(const YAML::Node & node) const { ros2_medkit_gateway::ScriptEntryConfig entry; entry.id = get_string(node, "id"); diff --git a/src/ros2_medkit_gateway/src/http/handlers/docs_handlers.cpp b/src/ros2_medkit_gateway/src/http/handlers/docs_handlers.cpp index fea4f8579..005741982 100644 --- a/src/ros2_medkit_gateway/src/http/handlers/docs_handlers.cpp +++ b/src/ros2_medkit_gateway/src/http/handlers/docs_handlers.cpp @@ -59,11 +59,12 @@ void DocsHandlers::write_error(httplib::Response & res, int status, const std::s DocsHandlers::DocsHandlers(HandlerContext & ctx, PluginManager * plugin_mgr, const openapi::RouteRegistry * route_registry, bool docs_enabled, std::string server_host, - int64_t server_port) + int64_t server_port, ServerIdentity identity) : ctx_(ctx) , generator_(std::make_unique(ctx, plugin_mgr, route_registry, std::move(server_host), - server_port)) - , docs_enabled_(docs_enabled) { + server_port, openapi::DocsCacheBounds{}, identity)) + , docs_enabled_(docs_enabled) + , server_name_(std::move(identity.server_name)) { } DocsHandlers::~DocsHandlers() = default; @@ -100,37 +101,60 @@ void DocsHandlers::handle_docs_any_path(const httplib::Request & req, httplib::R namespace { +std::string html_escape(const std::string & text) { + std::string out; + for (const char c : text) { + switch (c) { + case '&': + out += "&"; + break; + case '<': + out += "<"; + break; + case '>': + out += ">"; + break; + case '"': + out += """; + break; + default: + out += c; + } + } + return out; +} + // HTML page that loads embedded Swagger UI assets and points to /api/v1/docs -const std::string & get_embedded_html() { - static const std::string html = - "\n" - "\n" - "\n" - " \n" - " \n" - " ROS 2 Medkit Gateway - API Documentation\n" - " \n" - " \n" - "\n" - "\n" - "
\n" - " \n" - " \n" - " \n" - "\n" - "\n"; - return html; +std::string build_embedded_html(const std::string & server_name) { + return "\n" + "\n" + "\n" + " \n" + " \n" + " " + + html_escape(server_name) + + " - API Documentation\n" + " \n" + " \n" + "\n" + "\n" + "
\n" + " \n" + " \n" + " \n" + "\n" + "\n"; } } // namespace @@ -143,7 +167,7 @@ void DocsHandlers::handle_swagger_ui(const httplib::Request & /*req*/, httplib:: res.set_header("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"); - res.set_content(get_embedded_html(), "text/html"); + res.set_content(build_embedded_html(server_name_), "text/html"); } void DocsHandlers::handle_swagger_asset(const httplib::Request & req, httplib::Response & res) { diff --git a/src/ros2_medkit_gateway/src/http/handlers/health_handlers.cpp b/src/ros2_medkit_gateway/src/http/handlers/health_handlers.cpp index ee60acf18..5708b5ce4 100644 --- a/src/ros2_medkit_gateway/src/http/handlers/health_handlers.cpp +++ b/src/ros2_medkit_gateway/src/http/handlers/health_handlers.cpp @@ -299,8 +299,8 @@ http::Result HealthHandlers::get_root(const http::TypedReques ctx_.services().plugin_manager() && ctx_.services().plugin_manager()->has_plugins(); dto::RootOverview response; - response.name = "ROS 2 Medkit Gateway"; - response.version = kGatewayVersion; + response.name = identity_.server_name; + response.version = identity_.server_version; response.api_base = API_BASE_PATH; response.endpoints = std::move(endpoints); response.capabilities = capabilities; @@ -335,8 +335,8 @@ http::Result HealthHandlers::get_version_info(const http::Type try { // SOVD 7.4.1 compliant response format dto::VersionInfoVendor vendor; - vendor.version = kGatewayVersion; - vendor.name = "ros2_medkit"; + vendor.version = identity_.server_version; + vendor.name = identity_.vendor_name; dto::VersionInfoEntry entry; entry.version = kSovdVersion; // SOVD standard version diff --git a/src/ros2_medkit_gateway/src/http/rest_server.cpp b/src/ros2_medkit_gateway/src/http/rest_server.cpp index a64ae2275..2cbfd474a 100644 --- a/src/ros2_medkit_gateway/src/http/rest_server.cpp +++ b/src/ros2_medkit_gateway/src/http/rest_server.cpp @@ -189,8 +189,8 @@ RESTServer::RESTServer(GatewayServices & services, std::shared_ptr(*handler_ctx_, route_registry_.get(), - sse_client_tracker_, rest_config.docs_enabled); + health_handlers_ = std::make_unique( + *handler_ctx_, route_registry_.get(), sse_client_tracker_, rest_config.docs_enabled, rest_config); discovery_handlers_ = std::make_unique(*handler_ctx_); data_handlers_ = std::make_unique(*handler_ctx_); lifecycle_handlers_ = std::make_unique(*handler_ctx_, services_.plugin_manager(), @@ -241,9 +241,9 @@ RESTServer::RESTServer(GatewayServices & services, std::shared_ptr(*handler_ctx_, services_.script_manager()); } - docs_handlers_ = std::make_unique(*handler_ctx_, services_.plugin_manager(), - route_registry_.get(), rest_config.docs_enabled, - rest_config.server_host, rest_config.server_port); + docs_handlers_ = std::make_unique( + *handler_ctx_, services_.plugin_manager(), route_registry_.get(), rest_config.docs_enabled, + rest_config.server_host, rest_config.server_port, rest_config); // Set up global error handlers for SOVD GenericError compliance setup_global_error_handlers(); diff --git a/src/ros2_medkit_gateway/src/openapi/capability_generator.cpp b/src/ros2_medkit_gateway/src/openapi/capability_generator.cpp index bc2eabc53..9210f0eae 100644 --- a/src/ros2_medkit_gateway/src/openapi/capability_generator.cpp +++ b/src/ros2_medkit_gateway/src/openapi/capability_generator.cpp @@ -110,14 +110,15 @@ void for_each_operation(nlohmann::json & document, Visitor && visit) { CapabilityGenerator::CapabilityGenerator(handlers::HandlerContext & ctx, PluginManager * plugin_mgr, const RouteRegistry * route_registry, std::string server_host, - int64_t server_port, DocsCacheBounds bounds) + int64_t server_port, DocsCacheBounds bounds, ServerIdentity identity) : ctx_(ctx) , plugin_mgr_(plugin_mgr) , route_registry_(route_registry) , schema_builder_(*ctx.services().type_schemas()) , server_host_(std::move(server_host)) , server_port_(server_port) - , bounds_(bounds) { + , bounds_(bounds) + , identity_(std::move(identity)) { } // TODO(#272): Fix TOCTOU race - use compare-and-swap when storing cached specs @@ -330,7 +331,7 @@ nlohmann::json CapabilityGenerator::generate_root() const { } OpenApiSpecBuilder builder; - builder.info("ROS 2 Medkit Gateway", kGatewayVersion) + builder.info(identity_.server_name, identity_.server_version) .description( "SOVD-compatible REST API for ROS 2 diagnostics and control. " "See https://selfpatch.github.io/ros2_medkit/ for documentation.") @@ -459,7 +460,7 @@ nlohmann::json CapabilityGenerator::project(const nlohmann::json & served, const nlohmann::json CapabilityGenerator::build_subtree_document(const std::string & title, const nlohmann::json & paths) const { OpenApiSpecBuilder builder; - builder.info("ROS 2 Medkit Gateway - " + title, kGatewayVersion) + builder.info(identity_.server_name + " - " + title, identity_.server_version) .sovd_version(kSovdVersion) .server(build_server_url(), "Gateway server") // The projected operations carry whatever `security` their registration @@ -953,7 +954,7 @@ nlohmann::json CapabilityGenerator::generate_plugin_docs(const std::string & pat } OpenApiSpecBuilder builder; - builder.info("ROS 2 Medkit Gateway - Plugin", kGatewayVersion) + builder.info(identity_.server_name + " - Plugin", identity_.server_version) .sovd_version(kSovdVersion) .server(build_server_url(), "Gateway server") // Without the definition, the per-operation `security` a plugin diff --git a/src/ros2_medkit_gateway/src/openapi/capability_generator.hpp b/src/ros2_medkit_gateway/src/openapi/capability_generator.hpp index c08d3df3c..b6f52beb3 100644 --- a/src/ros2_medkit_gateway/src/openapi/capability_generator.hpp +++ b/src/ros2_medkit_gateway/src/openapi/capability_generator.hpp @@ -26,6 +26,7 @@ #include "route_registry.hpp" #include "schema_builder.hpp" +#include "ros2_medkit_gateway/core/config.hpp" #include "ros2_medkit_gateway/core/models/entity_types.hpp" namespace ros2_medkit_gateway { @@ -70,7 +71,8 @@ struct DocsCacheBounds { class CapabilityGenerator { public: CapabilityGenerator(handlers::HandlerContext & ctx, PluginManager * plugin_mgr, const RouteRegistry * route_registry, - std::string server_host, int64_t server_port, DocsCacheBounds bounds = DocsCacheBounds{}); + std::string server_host, int64_t server_port, DocsCacheBounds bounds = DocsCacheBounds{}, + ServerIdentity identity = {}); /// Number of documents currently cached. size_t cache_entry_count() const; @@ -237,6 +239,7 @@ class CapabilityGenerator { std::string server_host_; int64_t server_port_; DocsCacheBounds bounds_; + ServerIdentity identity_; // Generation-based document cache - invalidated when entity cache changes. //