Skip to content

Commit 4cf3724

Browse files
committed
fix(logging): avoid overriding global rcutils log handler
Signed-off-by: Esteve Fernandez <esteve@apache.org>
1 parent 9429ccc commit 4cf3724

3 files changed

Lines changed: 88 additions & 160 deletions

File tree

rclrs/src/logging.rs

Lines changed: 19 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -335,57 +335,37 @@ pub unsafe fn impl_log(
335335
static FORMAT_STRING: OnceLock<CString> = OnceLock::new();
336336
let format_string = FORMAT_STRING.get_or_init(|| CString::new("%s").unwrap());
337337

338-
let severity = severity.as_native();
338+
let severity = severity.as_native() as i32;
339339

340340
let _lifecycle = ENTITY_LIFECYCLE_MUTEX.lock().unwrap();
341341

342342
#[cfg(test)]
343-
{
344-
// If we are compiling for testing purposes, when the default log
345-
// output handler is being used we need to use the format_string,
346-
// but when our custom log output handler is being used we need to
347-
// pass the raw message string so that it can be viewed by the
348-
// custom log output handler, allowing us to use it for test assertions.
349-
if log_handler::is_using_custom_handler() {
350-
// We are using the custom log handler that is only used during
351-
// logging tests, so pass the raw message as the format string.
352-
unsafe {
353-
// SAFETY: The global mutex is locked as _lifecycle
354-
rcutils_log(
355-
&location,
356-
severity as i32,
357-
logger_name.as_ptr(),
358-
message.as_ptr(),
359-
);
360-
}
361-
} else {
362-
// We are using the normal log handler so call rcutils_log the normal way.
363-
unsafe {
364-
// SAFETY: The global mutex is locked as _lifecycle
365-
rcutils_log(
366-
&location,
367-
severity as i32,
368-
logger_name.as_ptr(),
369-
format_string.as_ptr(),
370-
message.as_ptr(),
371-
);
372-
}
373-
}
374-
}
375-
376-
#[cfg(not(test))]
343+
if log_handler::is_using_custom_handler()
344+
&& unsafe { rcutils_logging_logger_is_enabled_for(logger_name.as_ptr(), severity) }
377345
{
378346
unsafe {
379-
// SAFETY: The global mutex is locked as _lifecycle
380-
rcutils_log(
347+
// SAFETY: The global mutex is locked as _lifecycle and the pointers are valid for
348+
// the duration of this call.
349+
log_handler::dispatch_logging_output_handler(
381350
&location,
382-
severity as i32,
351+
severity,
383352
logger_name.as_ptr(),
384-
format_string.as_ptr(),
353+
log_handler::null_timestamp(),
385354
message.as_ptr(),
386355
);
387356
}
388357
}
358+
359+
unsafe {
360+
// SAFETY: The global mutex is locked as _lifecycle
361+
rcutils_log(
362+
&location,
363+
severity,
364+
logger_name.as_ptr(),
365+
format_string.as_ptr(),
366+
message.as_ptr(),
367+
);
368+
}
389369
};
390370

391371
match logger_name {

rclrs/src/logging/logging_configuration.rs

Lines changed: 66 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -59,39 +59,17 @@ pub(crate) mod log_handler {
5959
//! we need to figure out a way to process C-style formatting strings with
6060
//! a [`va_list`] from inside of Rust, which seems to be very messy.
6161
62-
use std::{
63-
borrow::Cow,
64-
ffi::CStr,
65-
sync::{
66-
atomic::{AtomicBool, Ordering},
67-
OnceLock,
68-
},
69-
};
70-
71-
use crate::{rcl_bindings::*, LogSeverity, ENTITY_LIFECYCLE_MUTEX};
72-
73-
/// Global variable that allows a custom log handler to be set. This log
74-
/// handler will be applied throughout the entire application and cannot be
75-
/// replaced with a different custom log handler. If you want to be able to
76-
/// change the log handler over the lifetime of your application, you should
77-
/// design your own custom handler with an Arc<Mutex<T>> inside that allows
78-
/// its own behavior to be modified.
79-
static LOGGING_OUTPUT_HANDLER: OnceLock<RawLogHandler> = OnceLock::new();
80-
81-
/// Alias for an arbitrary log handler that is compatible with raw rcl types
82-
pub(crate) type RawLogHandler = Box<
83-
dyn Fn(
84-
*const rcutils_log_location_t, // location
85-
std::os::raw::c_int, // severity
86-
*const std::os::raw::c_char, // logger name
87-
rcutils_time_point_value_t, // timestamp
88-
*const std::os::raw::c_char, // format
89-
*mut va_list, // formatting arguments
90-
)
91-
+ 'static
92-
+ Send
93-
+ Sync,
94-
>;
62+
use std::{borrow::Cow, cell::RefCell, ffi::CStr, sync::Arc};
63+
64+
use crate::{rcl_bindings::*, LogSeverity};
65+
66+
thread_local! {
67+
/// Thread-local variable that allows tests to observe log entries.
68+
static LOG_OBSERVER: RefCell<Option<LogObserver>> = RefCell::new(None);
69+
}
70+
71+
/// Alias for a test observer of log entries emitted through rclrs logging macros.
72+
pub(crate) type LogObserver = Arc<dyn Fn(LogEntry) + 'static + Send + Sync>;
9573

9674
/// This is an idiomatic representation of all the information for a log entry
9775
#[derive(Clone)]
@@ -137,115 +115,84 @@ pub(crate) mod log_handler {
137115
#[derive(Debug)]
138116
pub(crate) struct OutputHandlerAlreadySet;
139117

140-
static USING_CUSTOM_HANDLER: OnceLock<AtomicBool> = OnceLock::new();
141-
142118
/// Set an idiomatic log hander
143119
pub(crate) fn set_logging_output_handler(
144120
handler: impl Fn(LogEntry) + 'static + Send + Sync,
145121
) -> Result<(), OutputHandlerAlreadySet> {
146-
let raw_handler = Box::new(
147-
move |raw_location: *const rcutils_log_location_t,
148-
raw_severity: std::os::raw::c_int,
149-
raw_logger_name: *const std::os::raw::c_char,
150-
raw_timestamp: rcutils_time_point_value_t,
151-
raw_format: *const std::os::raw::c_char,
152-
// NOTE: In the rclrs logging test we are choosing to format
153-
// the full message in advance when using the custom handler,
154-
// so the format field always contains the finished formatted
155-
// message. Therefore we can just ignore the raw formatting
156-
// arguments.
157-
_raw_formatting_arguments: *mut va_list| {
158-
unsafe {
159-
// NOTE: We use .unwrap() extensively inside this function because
160-
// it only gets used during tests. We should reconsider this if
161-
// we ever make this public.
162-
let location = LogLocation {
163-
function_name: Cow::Borrowed(
164-
CStr::from_ptr((*raw_location).function_name)
165-
.to_str()
166-
.unwrap(),
167-
),
168-
file_name: Cow::Borrowed(
169-
CStr::from_ptr((*raw_location).file_name).to_str().unwrap(),
170-
),
171-
line_number: (*raw_location).line_number,
172-
};
173-
let severity = LogSeverity::from_native(raw_severity);
174-
let logger_name =
175-
Cow::Borrowed(CStr::from_ptr(raw_logger_name).to_str().unwrap());
176-
let timestamp: i64 = raw_timestamp;
177-
let message = Cow::Borrowed(CStr::from_ptr(raw_format).to_str().unwrap());
178-
handler(LogEntry {
179-
location,
180-
severity,
181-
logger_name,
182-
timestamp,
183-
message,
184-
});
185-
}
186-
},
187-
);
188-
189-
set_raw_logging_output_handler(raw_handler)
190-
}
191-
192-
/// Set the logging output handler directly
193-
pub(crate) fn set_raw_logging_output_handler(
194-
handler: RawLogHandler,
195-
) -> Result<(), OutputHandlerAlreadySet> {
196-
LOGGING_OUTPUT_HANDLER
197-
.set(handler)
198-
.map_err(|_| OutputHandlerAlreadySet)?;
199-
let _lifecycle = ENTITY_LIFECYCLE_MUTEX.lock().unwrap();
200-
unsafe {
201-
// SAFETY:
202-
// - We have locked the global mutex
203-
rcutils_logging_set_output_handler(Some(rclrs_logging_output_handler));
204-
}
205-
206-
USING_CUSTOM_HANDLER
207-
.get_or_init(|| AtomicBool::new(false))
208-
.store(true, Ordering::Release);
209-
Ok(())
122+
let observer: LogObserver = Arc::new(handler);
123+
LOG_OBSERVER.with(|handler_slot| {
124+
let mut handler_slot = handler_slot.borrow_mut();
125+
if handler_slot.is_some() {
126+
return Err(OutputHandlerAlreadySet);
127+
}
128+
*handler_slot = Some(observer);
129+
Ok(())
130+
})
210131
}
211132

212133
pub(crate) fn is_using_custom_handler() -> bool {
213-
USING_CUSTOM_HANDLER
214-
.get_or_init(|| AtomicBool::new(false))
215-
.load(Ordering::Acquire)
134+
LOG_OBSERVER.with(|handler_slot| handler_slot.borrow().is_some())
216135
}
217136

218-
/// This function exists so that we can give a raw function pointer to
219-
/// rcutils_logging_set_output_handler, which is needed by its API.
220-
unsafe extern "C" fn rclrs_logging_output_handler(
137+
/// Dispatch a log entry to the active test handler, if one is set.
138+
///
139+
/// SAFETY: The raw pointers must be valid for the duration of the call.
140+
pub(crate) unsafe fn dispatch_logging_output_handler(
221141
location: *const rcutils_log_location_t,
222142
severity: std::os::raw::c_int,
223143
logger_name: *const std::os::raw::c_char,
224144
timestamp: rcutils_time_point_value_t,
225145
message: *const std::os::raw::c_char,
226-
logging_output: *mut va_list,
227146
) {
228-
let handler = LOGGING_OUTPUT_HANDLER.get().unwrap();
229-
(*handler)(
147+
let handler = LOG_OBSERVER.with(|handler_slot| handler_slot.borrow().as_ref().cloned());
148+
let Some(handler) = handler else {
149+
return;
150+
};
151+
152+
let location = if location.is_null() {
153+
LogLocation {
154+
function_name: Cow::Borrowed(""),
155+
file_name: Cow::Borrowed(""),
156+
line_number: 0,
157+
}
158+
} else {
159+
LogLocation {
160+
function_name: Cow::Borrowed(
161+
CStr::from_ptr((*location).function_name).to_str().unwrap(),
162+
),
163+
file_name: Cow::Borrowed(CStr::from_ptr((*location).file_name).to_str().unwrap()),
164+
line_number: (*location).line_number,
165+
}
166+
};
167+
let severity = LogSeverity::from_native(severity);
168+
let logger_name = if logger_name.is_null() {
169+
Cow::Borrowed("")
170+
} else {
171+
Cow::Borrowed(CStr::from_ptr(logger_name).to_str().unwrap())
172+
};
173+
let message = if message.is_null() {
174+
Cow::Borrowed("")
175+
} else {
176+
Cow::Borrowed(CStr::from_ptr(message).to_str().unwrap())
177+
};
178+
179+
handler(LogEntry {
230180
location,
231181
severity,
232182
logger_name,
233183
timestamp,
234184
message,
235-
logging_output,
236-
);
185+
});
237186
}
238187

239188
/// Reset the logging output handler to the default one
240189
pub(crate) fn reset_logging_output_handler() {
241-
let _lifecycle = ENTITY_LIFECYCLE_MUTEX.lock().unwrap();
242-
unsafe {
243-
// SAFETY: The global mutex is locked. No other precondition is
244-
// required.
245-
rcutils_logging_set_output_handler(Some(rcl_logging_multiple_output_handler));
246-
}
247-
USING_CUSTOM_HANDLER
248-
.get_or_init(|| AtomicBool::new(false))
249-
.store(false, Ordering::Release);
190+
LOG_OBSERVER.with(|handler_slot| *handler_slot.borrow_mut() = None);
191+
}
192+
193+
pub(crate) fn null_timestamp() -> rcutils_time_point_value_t {
194+
// This value is only used by the Rust-side test hook. The real rcutils
195+
// output handler still receives the timestamp produced by rcutils_log.
196+
0
250197
}
251198
}

rclrs/src/node/node_options.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,10 @@ impl<'a> NodeOptions<'a> {
338338
// so we only need to explicitly initialize it on newer distros.
339339
#[cfg(not(ros_distro = "humble"))]
340340
if self.enable_rosout {
341-
// SAFETY: rcl_logging_rosout_enabled checks if rosout logging is globally enabled.
341+
let _lifecycle_lock = ENTITY_LIFECYCLE_MUTEX.lock().unwrap();
342+
// SAFETY: The entity lifecycle mutex is locked and rcl_logging_rosout_enabled checks
343+
// if rosout logging is globally enabled.
342344
if unsafe { rcl_logging_rosout_enabled() } {
343-
let _lifecycle_lock = ENTITY_LIFECYCLE_MUTEX.lock().unwrap();
344345
// SAFETY: The node has been successfully initialized and the
345346
// entity lifecycle mutex is locked.
346347
unsafe {

0 commit comments

Comments
 (0)