Linux Introspection Plugins
The ros2_medkit_linux_introspection package provides three plugins that enrich the
gateway with OS-level metadata for ROS 2 nodes. Each plugin implements the
IntrospectionProvider interface and registers vendor-specific REST endpoints on
Apps and Components.
procfs - reads
/procfor process info (PID, RSS, CPU ticks, threads, exe path, cmdline). Works on any Linux system.systemd - maps ROS 2 nodes to systemd units via
sd_pid_get_unit(), then queries unit properties (ActiveState, SubState, NRestarts, WatchdogUSec) via sd-bus. Requireslibsystemd.container - detects containerization via cgroup path analysis. Supports Docker, podman, and containerd. Reads cgroup v2 resource limits (
memory.max,cpu.max).
Each plugin maintains its own PID cache that maps ROS 2 node fully-qualified names to
Linux PIDs by scanning /proc. The cache refreshes on each discovery cycle and on
demand when the TTL expires.
Requirements
procfs: Linux only (reads
/procfilesystem). No extra dependencies.systemd: requires
libsystemd-devat build time, systemd at runtime. Skipped automatically iflibsystemdis not found during the build.container: requires cgroup v2, which is the default on modern kernels (Ubuntu 22.04+, Fedora 31+).
Building
The plugins build as part of the ros2_medkit colcon workspace:
source /opt/ros/jazzy/setup.bash
colcon build --packages-select ros2_medkit_linux_introspection
Verify the .so files are installed:
ls install/ros2_medkit_linux_introspection/lib/ros2_medkit_linux_introspection/
# libprocfs_introspection.so
# libsystemd_introspection.so (only if libsystemd was found)
# libcontainer_introspection.so
Note
The systemd plugin is conditionally built. If libsystemd-dev is not installed,
CMake prints a warning and skips it. Install with sudo apt install libsystemd-dev
on Ubuntu/Debian.
Configuration
Add plugins to gateway_params.yaml. You can enable any combination - each plugin
is independent:
ros2_medkit_gateway:
ros__parameters:
plugins: ["procfs", "systemd", "container"]
# Paths are relative to the colcon workspace root (where you run 'ros2 launch').
# Use absolute paths if launching from a different directory.
plugins.procfs.path: "install/ros2_medkit_linux_introspection/lib/ros2_medkit_linux_introspection/libprocfs_introspection.so"
plugins.procfs.pid_cache_ttl_seconds: 10
plugins.systemd.path: "install/ros2_medkit_linux_introspection/lib/ros2_medkit_linux_introspection/libsystemd_introspection.so"
plugins.systemd.pid_cache_ttl_seconds: 10
plugins.container.path: "install/ros2_medkit_linux_introspection/lib/ros2_medkit_linux_introspection/libcontainer_introspection.so"
plugins.container.pid_cache_ttl_seconds: 10
Or enable just one plugin:
ros2_medkit_gateway:
ros__parameters:
plugins: ["procfs"]
plugins.procfs.path: "/opt/ros2_medkit/lib/ros2_medkit_linux_introspection/libprocfs_introspection.so"
Configuration parameters (all plugins):
pid_cache_ttl_seconds(int, default 10)TTL in seconds for the PID cache. The cache maps ROS 2 node FQNs to PIDs by scanning
/proc. Lower values give fresher data but increase/procscan frequency.proc_root(string, default"/")Root path for
/procaccess. Primarily used for testing with synthetic/proctrees. In production, leave at the default.
See Plugin System for general plugin configuration details.
API Reference
Each plugin registers vendor-specific endpoints on Apps (individual nodes) and Components (aggregated across child nodes).
procfs Endpoints
GET /apps/{id}/x-medkit-procfs
Returns process-level metrics for a single ROS 2 node:
curl http://localhost:8080/api/v1/apps/temp_sensor/x-medkit-procfs | jq
{
"pid": 12345,
"ppid": 1,
"exe": "/opt/ros/jazzy/lib/demo_nodes_cpp/talker",
"cmdline": "/opt/ros/jazzy/lib/demo_nodes_cpp/talker --ros-args ...",
"rss_bytes": 15728640,
"vm_size_bytes": 268435456,
"threads": 4,
"cpu_user_ticks": 1500,
"cpu_system_ticks": 300,
"uptime_seconds": 3600
}
GET /components/{id}/x-medkit-procfs
Returns aggregated process info for all child Apps of a Component, deduplicated by PID.
Each entry includes a node_ids array listing the Apps that share the process:
curl http://localhost:8080/api/v1/components/sensor_suite/x-medkit-procfs | jq
{
"processes": [
{
"pid": 12345,
"ppid": 1,
"exe": "/opt/ros/jazzy/lib/sensor_pkg/sensor_node",
"cmdline": "/opt/ros/jazzy/lib/sensor_pkg/sensor_node --ros-args ...",
"rss_bytes": 15728640,
"vm_size_bytes": 268435456,
"threads": 4,
"cpu_user_ticks": 1500,
"cpu_system_ticks": 300,
"uptime_seconds": 3600,
"node_ids": ["temp_sensor", "rpm_sensor"]
}
]
}
systemd Endpoints
GET /apps/{id}/x-medkit-systemd
Returns the systemd unit managing the node’s process:
curl http://localhost:8080/api/v1/apps/temp_sensor/x-medkit-systemd | jq
{
"unit": "ros2-demo-temp-sensor.service",
"unit_type": "service",
"active_state": "active",
"sub_state": "running",
"restart_count": 0,
"watchdog_usec": 0
}
GET /components/{id}/x-medkit-systemd
Returns aggregated unit info for all child Apps, deduplicated by unit name:
curl http://localhost:8080/api/v1/components/sensor_suite/x-medkit-systemd | jq
{
"units": [
{
"unit": "ros2-demo.service",
"unit_type": "service",
"active_state": "active",
"sub_state": "running",
"restart_count": 0,
"watchdog_usec": 0,
"node_ids": ["temp_sensor", "rpm_sensor"]
}
]
}
container Endpoints
GET /apps/{id}/x-medkit-container
Returns container metadata for a node running inside a container:
curl http://localhost:8080/api/v1/apps/temp_sensor/x-medkit-container | jq
{
"container_id": "a1b2c3d4e5f6...",
"runtime": "docker",
"memory_limit_bytes": 536870912,
"cpu_quota_us": 100000,
"cpu_period_us": 100000
}
Note
The memory_limit_bytes, cpu_quota_us, and cpu_period_us fields are only
present when cgroup v2 resource limits are set. If no limits are configured, these
fields are omitted from the response.
GET /components/{id}/x-medkit-container
Returns aggregated container info for all child Apps, deduplicated by container ID:
curl http://localhost:8080/api/v1/components/sensor_suite/x-medkit-container | jq
{
"containers": [
{
"container_id": "a1b2c3d4e5f6...",
"runtime": "docker",
"memory_limit_bytes": 536870912,
"cpu_quota_us": 100000,
"cpu_period_us": 100000,
"node_ids": ["temp_sensor", "rpm_sensor"]
}
]
}
Error Responses
All endpoints return SOVD-compliant GenericError responses on failure. Entity
validation errors (404 for unknown entities) are handled automatically by
validate_entity_for_route(). Plugin-specific errors:
Code |
Error ID |
Description |
|---|---|---|
404 |
|
PID not found for node. The node may not be running, or the PID cache has not refreshed. |
503 |
|
Failed to read |
404 |
|
Node’s process is not managed by a systemd unit. It may have been started manually. |
503 |
|
Failed to query systemd properties via sd-bus. Check D-Bus socket access. |
404 |
|
Node’s process is not running inside a container (no container cgroup path detected). |
503 |
|
Failed to read cgroup info for the container. Check cgroup v2 filesystem access. |
Note
Component-level endpoints (/components/{id}/x-medkit-*) silently skip child Apps
that cannot be resolved. They return partial results rather than failing entirely.
Composable Nodes
When multiple ROS 2 nodes share a process (composable nodes / component containers), they share the same PID. The plugins handle this correctly:
procfs: the Component endpoint deduplicates by PID. A single process entry includes all node IDs that share it in the
node_idsarray.systemd: the Component endpoint deduplicates by unit name. Composable nodes in the same process always map to the same systemd unit.
container: the Component endpoint deduplicates by container ID. All nodes sharing a container appear in one entry.
App-level endpoints always return data for the single process hosting that node, regardless of how many other nodes share the same process.
Introspection Metadata
Plugin introspection data is accessed via the vendor extension endpoints registered by
each plugin (e.g., GET /apps/{id}/x-medkit-procfs). The IntrospectionProvider
interface enriches the discovery pipeline with capabilities and metadata fields, but the
detailed introspection data is served through the plugin’s own HTTP routes rather than
embedded in standard discovery responses.
Troubleshooting
PID lookup failures
The PID cache refreshes when its TTL expires (default 10 seconds). If a node was just started, the cache may not have picked it up yet. Causes:
Node started after the last cache refresh. Wait for the next refresh cycle.
Node name mismatch. The PID cache matches ROS 2 node FQNs (e.g.,
/sensors/temp) against/proc/{pid}/cmdlineentries. Ensure the node’s--ros-args -r __node:=and-r __ns:=match expectations.Node exited. The process may have crashed between the cache refresh and the REST request.
Composable nodes
Composable nodes loaded via ros2 component load into a component container do not
have __node:= or __ns:= arguments in their /proc/{pid}/cmdline. Node names
are set programmatically via rclcpp::NodeOptions rather than through command-line
arguments. As a result, the PID cache cannot resolve these nodes and they will appear
as unreachable in all introspection endpoints.
Workaround: Launch composable nodes via ros2 launch with explicit remapping
arguments (--ros-args -r __node:=<name> -r __ns:=<namespace>) instead of loading
them dynamically with ros2 component load.
Permission errors (procfs)
Most /proc/{pid} files are world-readable. However:
/proc/{pid}/exe(symlink to executable) requires same-user access orCAP_SYS_PTRACE. If the gateway runs as a different user, theexefield may be empty.In hardened environments with
hidepid=2mount option on/proc, only processes owned by the same user are visible. Run the gateway as root or in the same user namespace.
systemd bus access
The systemd plugin uses sd_bus_open_system() to connect to the system bus, typically
via /run/dbus/system_bus_socket. If the gateway runs in a container:
# Mount the host's D-Bus socket into the container
docker run -v /run/dbus/system_bus_socket:/run/dbus/system_bus_socket ...
# Or run privileged (not recommended for production)
docker run --privileged ...
Without system bus access, the systemd plugin will return 503 errors for all queries.
Container detection
The container plugin relies on cgroup v2 path analysis. To verify your system uses cgroup v2:
mount | grep cgroup2
# Should show: cgroup2 on /sys/fs/cgroup type cgroup2 (...)
# Or check a process's cgroup path
cat /proc/self/cgroup
# cgroup v2 output: "0::/user.slice/..."
Supported container runtimes and their cgroup path patterns:
Docker:
/docker/<64-char-hex>podman:
/libpod-<64-char-hex>.scopecontainerd (CRI):
/cri-containerd-<64-char-hex>.scope
If your runtime uses a different cgroup path format, the plugin will not detect the
container. The runtime field in the response indicates the detected runtime.