Skip to content

Commit ed1b0d9

Browse files
committed
feat(agnocastlib): add --enable-rosout-logs full support
Signed-off-by: PeterWrighten <peterwrighten@gmail.com>
1 parent 7bd97fe commit ed1b0d9

5 files changed

Lines changed: 150 additions & 7 deletions

File tree

src/agnocastlib/include/agnocast/node/agnocast_context.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ class Context
2626
return parsed_arguments_.is_valid() ? parsed_arguments_.get() : nullptr;
2727
}
2828

29+
bool is_rosout_enabled() const { return enable_rosout_logs_; }
30+
2931
private:
3032
bool initialized_ = false;
33+
bool enable_rosout_logs_ = false;
3134
ParsedArguments parsed_arguments_;
3235
};
3336

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#pragma once
2+
3+
namespace agnocast
4+
{
5+
class Node;
6+
7+
// Sets up a global /rosout publisher and installs a custom rcutils output handler
8+
// that chains the existing handler and publishes log messages to /rosout.
9+
// Only the first call creates the publisher; subsequent calls are no-ops.
10+
void setup_rosout_handler(Node * node);
11+
12+
} // namespace agnocast

src/agnocastlib/src/node/agnocast_context.cpp

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,22 @@ void Context::init(int argc, char const * const * argv)
4848
rcl_reset_error();
4949
}
5050

51-
// Apply --disable-stdout-logs if present within --ros-args scope.
52-
// There is no public rcl API to extract this flag from rcl_arguments_t, so we scan argv directly.
53-
// rcl_logging_configure() cannot be used as it would initialize spdlog and attempt to set up a
54-
// rosout publisher (which requires rcl_node_t), neither of which exist in agnocast.
51+
// Apply --disable-stdout-logs and detect --enable-rosout-logs within --ros-args scope.
52+
// There is no public rcl API to extract these flags from rcl_arguments_t, so we scan argv
53+
// directly. rcl_logging_configure() cannot be used as it would initialize spdlog and attempt to
54+
// set up a rosout publisher (which requires rcl_node_t), neither of which exist in agnocast.
5555
bool in_ros_args = false;
5656
for (const auto & arg : args) {
5757
if (arg == "--ros-args") {
5858
in_ros_args = true;
5959
} else if (arg == "--") {
6060
in_ros_args = false;
61-
} else if (in_ros_args && arg == "--disable-stdout-logs") {
62-
rcutils_logging_set_output_handler(noop_log_output_handler);
63-
break;
61+
} else if (in_ros_args) {
62+
if (arg == "--disable-stdout-logs") {
63+
rcutils_logging_set_output_handler(noop_log_output_handler);
64+
} else if (arg == "--enable-rosout-logs") {
65+
enable_rosout_logs_ = true;
66+
}
6467
}
6568
}
6669

src/agnocastlib/src/node/agnocast_node.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "agnocast/agnocast_tracepoint_wrapper.h"
44
#include "agnocast/node/agnocast_arguments.hpp"
55
#include "agnocast/node/agnocast_context.hpp"
6+
#include "agnocast/node/agnocast_rosout.hpp"
67

78
#include <rcl/time.h>
89

@@ -41,6 +42,13 @@ Node::Node(
4142
node_time_source_ = std::make_shared<node_interfaces::NodeTimeSource>(node_clock_, this);
4243

4344
node_services_ = std::make_shared<node_interfaces::NodeServices>(node_base_);
45+
46+
// Set up rosout publisher and handler if --enable-rosout-logs was specified.
47+
// Must be after node_base_, node_topics_, and node_parameters_ are initialized
48+
// (required by the publisher constructor). Only the first Node triggers setup.
49+
if (g_context.is_rosout_enabled()) {
50+
setup_rosout_handler(this);
51+
}
4452
}
4553

4654
} // namespace agnocast
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#include "agnocast/node/agnocast_rosout.hpp"
2+
3+
#include "agnocast/agnocast_publisher.hpp"
4+
#include "agnocast/node/agnocast_node.hpp"
5+
6+
#include <rcl_interfaces/msg/log.hpp>
7+
#include <rcutils/logging.h>
8+
9+
#include <atomic>
10+
#include <cstdarg>
11+
#include <cstdio>
12+
#include <mutex>
13+
14+
namespace agnocast
15+
{
16+
17+
static Publisher<rcl_interfaces::msg::Log>::SharedPtr g_rosout_pub;
18+
static std::mutex g_rosout_mtx;
19+
static std::atomic<bool> g_in_handler{false};
20+
static std::atomic<bool> g_rosout_initialized{false};
21+
static rcutils_logging_output_handler_t g_original_handler = nullptr;
22+
23+
static uint8_t severity_to_log_level(int severity)
24+
{
25+
switch (severity) {
26+
case RCUTILS_LOG_SEVERITY_DEBUG:
27+
return rcl_interfaces::msg::Log::DEBUG;
28+
case RCUTILS_LOG_SEVERITY_INFO:
29+
return rcl_interfaces::msg::Log::INFO;
30+
case RCUTILS_LOG_SEVERITY_WARN:
31+
return rcl_interfaces::msg::Log::WARN;
32+
case RCUTILS_LOG_SEVERITY_ERROR:
33+
return rcl_interfaces::msg::Log::ERROR;
34+
case RCUTILS_LOG_SEVERITY_FATAL:
35+
return rcl_interfaces::msg::Log::FATAL;
36+
default:
37+
return rcl_interfaces::msg::Log::INFO;
38+
}
39+
}
40+
41+
static void rosout_output_handler(
42+
const rcutils_log_location_t * location, int severity, const char * name,
43+
rcutils_time_point_value_t timestamp, const char * format, va_list * args)
44+
{
45+
// Chain the original handler (console or noop, depending on --disable-stdout-logs).
46+
// Pass a copy because the handler may consume the va_list via vsnprintf.
47+
if (g_original_handler) {
48+
va_list args_for_original;
49+
va_copy(args_for_original, *args);
50+
g_original_handler(location, severity, name, timestamp, format, &args_for_original);
51+
va_end(args_for_original);
52+
}
53+
54+
// Recursion guard: publishing may trigger log calls internally
55+
bool expected = false;
56+
if (!g_in_handler.compare_exchange_strong(expected, true)) {
57+
return;
58+
}
59+
60+
{
61+
std::lock_guard<std::mutex> lock(g_rosout_mtx);
62+
if (g_rosout_pub) {
63+
auto loaned_msg = g_rosout_pub->borrow_loaned_message();
64+
65+
loaned_msg->stamp.sec = static_cast<int32_t>(timestamp / 1000000000LL);
66+
loaned_msg->stamp.nanosec = static_cast<uint32_t>(timestamp % 1000000000LL);
67+
loaned_msg->level = severity_to_log_level(severity);
68+
loaned_msg->name = (name != nullptr) ? name : "";
69+
70+
// Format the message string from va_list
71+
va_list args_copy;
72+
va_copy(args_copy, *args);
73+
int len = vsnprintf(nullptr, 0, format, args_copy);
74+
va_end(args_copy);
75+
if (len >= 0) {
76+
std::string msg_str(static_cast<size_t>(len), '\0');
77+
va_copy(args_copy, *args);
78+
vsnprintf(&msg_str[0], static_cast<size_t>(len) + 1, format, args_copy);
79+
va_end(args_copy);
80+
loaned_msg->msg = std::move(msg_str);
81+
}
82+
83+
if (location != nullptr) {
84+
loaned_msg->file = (location->file_name != nullptr) ? location->file_name : "";
85+
loaned_msg->function = (location->function_name != nullptr) ? location->function_name : "";
86+
loaned_msg->line = location->line_number;
87+
}
88+
89+
g_rosout_pub->publish(std::move(loaned_msg));
90+
}
91+
}
92+
93+
g_in_handler.store(false);
94+
}
95+
96+
void setup_rosout_handler(Node * node)
97+
{
98+
// Only set up once per process, even with multiple nodes
99+
bool expected = false;
100+
if (!g_rosout_initialized.compare_exchange_strong(expected, true)) {
101+
return;
102+
}
103+
104+
auto pub = node->create_publisher<rcl_interfaces::msg::Log>(
105+
"/rosout", rclcpp::QoS(100).reliable().transient_local());
106+
107+
{
108+
std::lock_guard<std::mutex> lock(g_rosout_mtx);
109+
g_rosout_pub = pub;
110+
}
111+
112+
// Capture the current handler (console or noop) before replacing
113+
g_original_handler = rcutils_logging_get_output_handler();
114+
rcutils_logging_set_output_handler(rosout_output_handler);
115+
}
116+
117+
} // namespace agnocast

0 commit comments

Comments
 (0)