git2/tracing.rs
1use std::{
2 ffi::CStr,
3 sync::atomic::{AtomicPtr, Ordering},
4};
5
6use libc::{c_char, c_int};
7
8use crate::{raw, util::Binding, Error};
9
10/// Available tracing levels. When tracing is set to a particular level,
11/// callers will be provided tracing at the given level and all lower levels.
12#[derive(Copy, Clone, Debug)]
13pub enum TraceLevel {
14 /// No tracing will be performed.
15 None,
16
17 /// Severe errors that may impact the program's execution
18 Fatal,
19
20 /// Errors that do not impact the program's execution
21 Error,
22
23 /// Warnings that suggest abnormal data
24 Warn,
25
26 /// Informational messages about program execution
27 Info,
28
29 /// Detailed data that allows for debugging
30 Debug,
31
32 /// Exceptionally detailed debugging data
33 Trace,
34}
35
36impl Binding for TraceLevel {
37 type Raw = raw::git_trace_level_t;
38 unsafe fn from_raw(raw: raw::git_trace_level_t) -> Self {
39 match raw {
40 raw::GIT_TRACE_NONE => Self::None,
41 raw::GIT_TRACE_FATAL => Self::Fatal,
42 raw::GIT_TRACE_ERROR => Self::Error,
43 raw::GIT_TRACE_WARN => Self::Warn,
44 raw::GIT_TRACE_INFO => Self::Info,
45 raw::GIT_TRACE_DEBUG => Self::Debug,
46 raw::GIT_TRACE_TRACE => Self::Trace,
47 _ => panic!("Unknown git trace level"),
48 }
49 }
50 fn raw(&self) -> raw::git_trace_level_t {
51 match *self {
52 Self::None => raw::GIT_TRACE_NONE,
53 Self::Fatal => raw::GIT_TRACE_FATAL,
54 Self::Error => raw::GIT_TRACE_ERROR,
55 Self::Warn => raw::GIT_TRACE_WARN,
56 Self::Info => raw::GIT_TRACE_INFO,
57 Self::Debug => raw::GIT_TRACE_DEBUG,
58 Self::Trace => raw::GIT_TRACE_TRACE,
59 }
60 }
61}
62
63/// Callback type used to pass tracing events to the subscriber.
64/// see `trace_set` to register a subscriber.
65pub type TracingCb = fn(TraceLevel, &[u8]);
66
67/// Use an atomic pointer to store the global tracing subscriber function.
68static CALLBACK: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut());
69
70/// Set the global subscriber called when libgit2 produces a tracing message.
71pub fn trace_set(level: TraceLevel, cb: TracingCb) -> Result<(), Error> {
72 // Store the callback in the global atomic.
73 CALLBACK.store(cb as *mut (), Ordering::SeqCst);
74
75 // git_trace_set returns 0 if there was no error.
76 let return_code: c_int = unsafe { raw::git_trace_set(level.raw(), Some(tracing_cb_c)) };
77
78 if return_code != 0 {
79 Err(Error::last_error(return_code))
80 } else {
81 Ok(())
82 }
83}
84
85/// The tracing callback we pass to libgit2 (C ABI compatible).
86extern "C" fn tracing_cb_c(level: raw::git_trace_level_t, msg: *const c_char) {
87 // Load the callback function pointer from the global atomic.
88 let cb: *mut () = CALLBACK.load(Ordering::SeqCst);
89
90 // Transmute the callback pointer into the function pointer we know it to be.
91 //
92 // SAFETY: We only ever set the callback pointer with something cast from a TracingCb
93 // so transmuting back to a TracingCb is safe. This is notably not an integer-to-pointer
94 // transmute as described in the mem::transmute documentation and is in-line with the
95 // example in that documentation for casing between *const () to fn pointers.
96 let cb: TracingCb = unsafe { std::mem::transmute(cb) };
97
98 // If libgit2 passes us a message that is null, drop it and do not pass it to the callback.
99 // This is to avoid ever exposing rust code to a null ref, which would be Undefined Behavior.
100 if msg.is_null() {
101 return;
102 }
103
104 // Convert the message from a *const c_char to a &[u8] and pass it to the callback.
105 //
106 // SAFETY: We've just checked that the pointer is not null. The other safety requirements are left to
107 // libgit2 to enforce -- namely that it gives us a valid, nul-terminated, C string, that that string exists
108 // entirely in one allocation, that the string will not be mutated once passed to us, and that the nul-terminator is
109 // within isize::MAX bytes from the given pointers data address.
110 let msg: &CStr = unsafe { CStr::from_ptr(msg) };
111
112 // Convert from a CStr to &[u8] to pass to the rust code callback.
113 let msg: &[u8] = CStr::to_bytes(msg);
114
115 // Do not bother with wrapping any of the following calls in `panic::wrap`:
116 //
117 // The previous implementation used `panic::wrap` here but never called `panic::check` to determine if the
118 // trace callback had panicked, much less what caused it.
119 //
120 // This had the potential to lead to lost errors/unwinds, confusing to debugging situations, and potential issues
121 // catching panics in other parts of the `git2-rs` codebase.
122 //
123 // Instead, we simply call the next two lines, both of which may panic, directly. We can rely on the
124 // `extern "C"` semantics to appropriately catch the panics generated here and abort the process:
125 //
126 // Per <https://doc.rust-lang.org/std/panic/fn.catch_unwind.html>:
127 // > Rust functions that are expected to be called from foreign code that does not support
128 // > unwinding (such as C compiled with -fno-exceptions) should be defined using extern "C", which ensures
129 // > that if the Rust code panics, it is automatically caught and the process is aborted. If this is the desired
130 // > behavior, it is not necessary to use catch_unwind explicitly. This function should instead be used when
131 // > more graceful error-handling is needed.
132
133 // Convert the raw trace level into a type we can pass to the rust callback fn.
134 //
135 // SAFETY: Currently the implementation of this function (above) may panic, but is only marked as unsafe to match
136 // the trait definition, thus we can consider this call safe.
137 let level: TraceLevel = unsafe { Binding::from_raw(level) };
138
139 // Call the user-supplied callback (which may panic).
140 (cb)(level, msg);
141}
142
143#[cfg(test)]
144mod tests {
145 use super::TraceLevel;
146
147 // Test that using the above function to set a tracing callback doesn't panic.
148 #[test]
149 fn smoke() {
150 super::trace_set(TraceLevel::Trace, |level, msg| {
151 dbg!(level, msg);
152 })
153 .expect("libgit2 can set global trace callback");
154 }
155}