Skip to main content

steamkit/util/
debug_log.rs

1//! The global debug log. Listeners are either trait objects or plain functions,
2//! and messages are also sent on a broadcast channel for anyone who'd rather
3//! subscribe.
4
5use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
6use std::sync::{Arc, OnceLock, RwLock};
7
8use tokio::sync::broadcast;
9
10/// Capacity of the broadcast channel.
11const BROADCAST_CAPACITY: usize = 1024;
12
13/// A message that was written to the debug log.
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct DebugLogMessage {
16    /// The category of the message.
17    pub category: String,
18    /// The message itself.
19    pub message: String,
20}
21
22/// A debug log listener.
23pub trait IDebugListener: Send + Sync {
24    /// Called for every line written to the debug log.
25    fn write_line(&self, category: &str, msg: &str);
26}
27
28/// The global state of the debug log.
29struct DebugLogState {
30    /// Whether debug logging is enabled.
31    enabled: AtomicBool,
32    /// The registered listener objects.
33    listeners: RwLock<Vec<Arc<dyn IDebugListener>>>,
34    /// The registered listener functions, with their handles.
35    functions: RwLock<Vec<(u64, Arc<dyn Fn(&str, &str) + Send + Sync>)>>,
36    /// The handle the next listener function gets.
37    next_function_id: AtomicU64,
38    /// The channel log messages are broadcast on.
39    sender: broadcast::Sender<DebugLogMessage>,
40}
41
42impl DebugLogState {
43    fn new() -> DebugLogState {
44        let (sender, _) = broadcast::channel(BROADCAST_CAPACITY);
45
46        DebugLogState {
47            // On in debug builds, off in release builds.
48            enabled: AtomicBool::new(cfg!(debug_assertions)),
49            listeners: RwLock::new(Vec::new()),
50            functions: RwLock::new(Vec::new()),
51            next_function_id: AtomicU64::new(1),
52            sender,
53        }
54    }
55}
56
57/// The global debug log state, created on first use.
58fn state() -> &'static DebugLogState {
59    static STATE: OnceLock<DebugLogState> = OnceLock::new();
60
61    STATE.get_or_init(DebugLogState::new)
62}
63
64/// The global debug log.
65#[derive(Debug)]
66pub struct DebugLog;
67
68impl DebugLog {
69    /// Whether debug logging is on. Defaults to on in debug builds and off in
70    /// release builds.
71    pub fn enabled() -> bool {
72        state().enabled.load(Ordering::Relaxed)
73    }
74
75    /// Turns debug logging on or off.
76    pub fn set_enabled(enabled: bool) {
77        state().enabled.store(enabled, Ordering::Relaxed);
78    }
79
80    /// Adds a listener.
81    pub fn add_listener(listener: Arc<dyn IDebugListener>) {
82        state().listeners.write().unwrap_or_else(|error| error.into_inner()).push(listener);
83    }
84
85    /// Adds a listener function. Closures can't be compared, so use the
86    /// returned handle with [`DebugLog::remove_listener_function`] to remove it.
87    pub fn add_listener_function(
88        action: impl Fn(&str, &str) + Send + Sync + 'static,
89    ) -> u64 {
90        let id = state().next_function_id.fetch_add(1, Ordering::Relaxed);
91
92        state()
93            .functions
94            .write()
95            .unwrap_or_else(|error| error.into_inner())
96            .push((id, Arc::new(action)));
97
98        id
99    }
100
101    /// Removes a listener (compared by identity).
102    pub fn remove_listener(listener: &Arc<dyn IDebugListener>) {
103        let mut listeners = state().listeners.write().unwrap_or_else(|error| error.into_inner());
104
105        if let Some(index) = listeners
106            .iter()
107            .position(|registered| Arc::ptr_eq(registered, listener))
108        {
109            listeners.remove(index);
110        }
111    }
112
113    /// Removes a listener function added with
114    /// [`DebugLog::add_listener_function`].
115    pub fn remove_listener_function(id: u64) {
116        let mut functions = state().functions.write().unwrap_or_else(|error| error.into_inner());
117
118        if let Some(index) = functions.iter().position(|(registered, _)| *registered == id) {
119            functions.remove(index);
120        }
121    }
122
123    /// Removes all listeners.
124    pub fn clear_listeners() {
125        state()
126            .listeners
127            .write()
128            .unwrap_or_else(|error| error.into_inner())
129            .clear();
130
131        state()
132            .functions
133            .write()
134            .unwrap_or_else(|error| error.into_inner())
135            .clear();
136    }
137
138    /// Subscribes to debug log messages. A receiver that falls behind gets
139    /// `RecvError::Lagged` and misses messages.
140    pub fn subscribe() -> broadcast::Receiver<DebugLogMessage> {
141        state().sender.subscribe()
142    }
143
144    /// Writes a line to the debug log.
145    pub fn write_line(category: &str, msg: &str) {
146        let state = state();
147
148        if !state.enabled.load(Ordering::Relaxed) {
149            return;
150        }
151
152        let message = DebugLogMessage {
153            category: category.to_string(),
154            message: msg.to_string(),
155        };
156
157        // Sending only fails when nobody's subscribed, which is fine.
158        let _ = state.sender.send(message);
159
160        let listeners = state.listeners.read().unwrap_or_else(|error| error.into_inner());
161
162        for listener in listeners.iter() {
163            listener.write_line(category, msg);
164        }
165
166        drop(listeners);
167
168        let functions = state.functions.read().unwrap_or_else(|error| error.into_inner());
169
170        for (_, action) in functions.iter() {
171            action(category, msg);
172        }
173    }
174
175    /// Writes a line to the debug log, filling `{0}`, `{1}`, ... in `msg` from
176    /// `args` (`{{` and `}}` are literal braces). Format specifiers like
177    /// `{0:X}` aren't supported and are left as they are.
178    pub fn write_line_args(category: &str, msg: &str, args: &[&str]) {
179        DebugLog::write_line(category, &format_with_args(msg, args));
180    }
181
182    /// If `condition` is false, writes `message` to the debug log.
183    pub fn assert(condition: bool, category: &str, message: &str) {
184        if !condition {
185            DebugLog::write_line(category, &format!("Assertion Failed! {message}"));
186        }
187    }
188}
189
190/// Fills `{0}`, `{1}`, ... in `msg` from `args`. `{{` and `}}` are literal
191/// braces; placeholders with a format specifier or alignment are left as they
192/// are.
193pub fn format_with_args(msg: &str, args: &[&str]) -> String {
194    let mut result = String::with_capacity(msg.len());
195    let mut characters = msg.chars().peekable();
196
197    while let Some(character) = characters.next() {
198        match character {
199            '{' => {
200                if characters.peek() == Some(&'{') {
201                    characters.next();
202                    result.push('{');
203                    continue;
204                }
205
206                let mut placeholder = String::new();
207                let mut closed = false;
208
209                for character in characters.by_ref() {
210                    if character == '}' {
211                        closed = true;
212                        break;
213                    }
214
215                    placeholder.push(character);
216                }
217
218                if !closed {
219                    // Unterminated placeholder: copy the rest as-is.
220                    result.push('{');
221                    result.push_str(&placeholder);
222                    break;
223                }
224
225                match placeholder.parse::<usize>() {
226                    Ok(index) => match args.get(index) {
227                        Some(value) => result.push_str(value),
228                        None => {
229                            result.push('{');
230                            result.push_str(&placeholder);
231                            result.push('}');
232                        }
233                    },
234                    Err(_) => {
235                        result.push('{');
236                        result.push_str(&placeholder);
237                        result.push('}');
238                    }
239                }
240            }
241            '}' => {
242                if characters.peek() == Some(&'}') {
243                    characters.next();
244                    result.push('}');
245                    continue;
246                }
247
248                result.push('}');
249            }
250            _ => result.push(character),
251        }
252    }
253
254    result
255}