Skip to main content

cranpose_render_common/
debug_toggles.rs

1use std::{
2    collections::BTreeMap,
3    ffi::{OsStr, OsString},
4    str::FromStr,
5    sync::{Mutex, OnceLock, PoisonError, atomic::Ordering},
6};
7
8use portable_atomic::AtomicU64;
9
10static GENERATION: AtomicU64 = AtomicU64::new(0);
11
12/// A debug toggle cached until an override changes. Unset values can be
13/// read without acquiring the cache lock.
14pub struct DebugToggle {
15    name: &'static str,
16    cached: Mutex<Option<(u64, Option<String>)>>,
17    absent_generation: AtomicU64,
18}
19
20impl DebugToggle {
21    /// Creates a toggle named after the environment variable `name`; a programmatic override
22    /// of the same name takes precedence over the environment.
23    pub const fn new(name: &'static str) -> Self {
24        Self {
25            name,
26            cached: Mutex::new(None),
27            absent_generation: AtomicU64::new(u64::MAX),
28        }
29    }
30
31    /// Reads the toggle's value through `read`.
32    pub fn with<R>(&self, read: impl FnOnce(Option<&str>) -> R) -> R {
33        let generation = GENERATION.load(Ordering::Acquire);
34        if self.absent_generation.load(Ordering::Acquire) == generation {
35            return read(None);
36        }
37        let mut cached = self.cached.lock().unwrap_or_else(PoisonError::into_inner);
38        if cached.as_ref().is_none_or(|(seen, _)| *seen != generation) {
39            *cached = Some((generation, debug_toggle(self.name)));
40        }
41        let value = cached.as_ref().and_then(|(_, value)| value.as_deref());
42        if value.is_none() {
43            self.absent_generation.store(generation, Ordering::Release);
44        }
45        read(value)
46    }
47
48    /// Whether the toggle holds any value.
49    pub fn is_set(&self) -> bool {
50        self.with(|value| value.is_some())
51    }
52
53    /// Whether the toggle is switched on: `1`, `true` or `yes`.
54    pub fn flag(&self) -> bool {
55        self.with(|value| matches!(value, Some("1" | "true" | "yes")))
56    }
57
58    /// Whether the toggle holds exactly `expected`.
59    pub fn equals(&self, expected: &str) -> bool {
60        self.with(|value| value == Some(expected))
61    }
62
63    /// The toggle parsed as `T`, when set and well formed.
64    pub fn parse<T: FromStr>(&self) -> Option<T> {
65        self.with(|value| value.and_then(|value| value.parse().ok()))
66    }
67}
68
69fn overrides() -> &'static Mutex<BTreeMap<&'static str, OsString>> {
70    static OVERRIDES: OnceLock<Mutex<BTreeMap<&'static str, OsString>>> = OnceLock::new();
71    OVERRIDES.get_or_init(|| Mutex::new(BTreeMap::new()))
72}
73
74#[doc(hidden)]
75pub fn debug_toggle(name: &'static str) -> Option<String> {
76    let map = overrides().lock().unwrap_or_else(PoisonError::into_inner);
77    if let Some(value) = map.get(name) {
78        return value.to_str().map(str::to_owned);
79    }
80    drop(map);
81    std::env::var(name).ok()
82}
83
84#[doc(hidden)]
85pub fn debug_toggle_os(name: &'static str) -> Option<OsString> {
86    let map = overrides().lock().unwrap_or_else(PoisonError::into_inner);
87    if let Some(value) = map.get(name) {
88        return Some(value.clone());
89    }
90    drop(map);
91    std::env::var_os(name)
92}
93
94#[doc(hidden)]
95pub fn set_debug_toggle(name: &'static str, value: Option<&str>) {
96    set_debug_toggle_os(name, value.map(OsStr::new));
97}
98
99#[doc(hidden)]
100pub fn set_debug_toggle_os(name: &'static str, value: Option<&OsStr>) {
101    let mut map = overrides().lock().unwrap_or_else(PoisonError::into_inner);
102    match value {
103        Some(value) => map.insert(name, value.to_owned()),
104        None => map.remove(name),
105    };
106    GENERATION.fetch_add(1, Ordering::Release);
107}
108
109#[cfg(all(test, unix))]
110#[path = "tests/debug_toggles_tests.rs"]
111mod tests;