From c08de74e7217945af9fc7c9871ef0560adaffb91 Mon Sep 17 00:00:00 2001 From: Michel de Graaf Date: Mon, 7 Sep 2026 17:27:44 +0200 Subject: [PATCH] fix(lib): keep the log level in an atomic and let hosts change it The level lived in a `static mut` read from every logging thread and written by `init`, an unsynchronised access the compiler only tolerates inside `unsafe`. It is an atomic now, read without `unsafe`, and a host can raise or lower it while running through `set_log_level`. --- soulseek-rs-lib/src/utils/logger.rs | 51 +++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/soulseek-rs-lib/src/utils/logger.rs b/soulseek-rs-lib/src/utils/logger.rs index 208dd73..ea49656 100644 --- a/soulseek-rs-lib/src/utils/logger.rs +++ b/soulseek-rs-lib/src/utils/logger.rs @@ -3,7 +3,7 @@ use std::fs::{File, OpenOptions}; use std::io::Write; use std::sync::{ Mutex, Once, - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicU8, Ordering}, }; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -16,7 +16,23 @@ pub enum LogLevel { } static INIT: Once = Once::new(); -static mut LOG_LEVEL: LogLevel = LogLevel::Warn; +static LOG_LEVEL: AtomicU8 = AtomicU8::new(LogLevel::Warn as u8); + +/// Change the level at runtime, from any thread. +pub fn set_log_level(level: LogLevel) { + LOG_LEVEL.store(level as u8, Ordering::Relaxed); +} + +#[must_use] +pub fn log_level() -> LogLevel { + match LOG_LEVEL.load(Ordering::Relaxed) { + 0 => LogLevel::Error, + 2 => LogLevel::Info, + 3 => LogLevel::Debug, + 4 => LogLevel::Trace, + _ => LogLevel::Warn, + } +} static BUFFER: Mutex> = Mutex::new(Vec::new()); static BUFFERING: AtomicBool = AtomicBool::new(false); @@ -29,15 +45,13 @@ pub fn init() { .unwrap_or_else(|_| "WARN".to_string()) .to_uppercase(); - unsafe { - LOG_LEVEL = match level.as_str() { - "ERROR" => LogLevel::Error, - "INFO" => LogLevel::Info, - "DEBUG" | "VERBOSE" => LogLevel::Debug, // Map VERBOSE to DEBUG - "TRACE" => LogLevel::Trace, - _ => LogLevel::Warn, // "WARN" or default - }; - } + set_log_level(match level.as_str() { + "ERROR" => LogLevel::Error, + "INFO" => LogLevel::Info, + "DEBUG" | "VERBOSE" => LogLevel::Debug, + "TRACE" => LogLevel::Trace, + _ => LogLevel::Warn, + }); // Initialize log file if LOG_FILE env var is set if let Ok(log_file_path) = env::var("LOG_FILE") { @@ -85,8 +99,8 @@ fn has_log_file() -> bool { } pub fn log(level: LogLevel, message: &str) { - unsafe { - if level <= LOG_LEVEL { + if level <= log_level() { + { let (name, colour) = match level { LogLevel::Error => ("ERROR", "\x1b[31m"), // Red LogLevel::Warn => ("WARN", "\x1b[33m"), // Yellow @@ -239,7 +253,16 @@ macro_rules! trace { #[cfg(test)] mod tests { - use super::{LogSink, choose_sink}; + use super::{LogLevel, LogSink, choose_sink, log_level, set_log_level}; + + #[test] + fn the_level_can_be_changed_while_running() { + let before = log_level(); + set_log_level(LogLevel::Trace); + assert_eq!(log_level(), LogLevel::Trace); + set_log_level(before); + assert_eq!(log_level(), before); + } #[test] fn a_configured_file_bypasses_buffering_so_lines_are_not_duplicated() {