use core::ffi::c_void;
use core::mem::ManuallyDrop;
use core::ops::Deref;
use core::ops::DerefMut;
use std::io::Write;
use std::sync::PoisonError;
use polyplug_abi::runtime::RuntimeConfig;
use polyplug_abi::types::{LogLevel, StringView};
#[derive(Clone, Copy)]
pub struct LoggerHandle {
callback: Option<unsafe extern "C" fn(*mut c_void, u32, StringView, StringView)>,
user_data: *mut c_void,
max_level: u32,
}
unsafe impl Send for LoggerHandle {}
unsafe impl Sync for LoggerHandle {}
impl LoggerHandle {
pub const fn default_stderr() -> LoggerHandle {
LoggerHandle {
callback: None,
user_data: core::ptr::null_mut(),
max_level: LogLevel::Warn as u32,
}
}
pub(crate) fn from_config(config: &RuntimeConfig) -> LoggerHandle {
LoggerHandle {
callback: config.log,
user_data: config.log_user_data,
max_level: config.log_max_level,
}
}
pub(crate) fn enabled(&self, level: LogLevel) -> bool {
let effective_max: u32 = match self.callback {
Some(_) => self.max_level,
None => LogLevel::Warn as u32,
};
(level as u32) <= effective_max
}
pub fn log<F>(&self, level: LogLevel, scope: &str, message: F)
where
F: FnOnce() -> String,
{
if !self.enabled(level) {
return;
}
let msg: String = message();
match self.callback {
Some(callback) => {
let scope_view = StringView {
ptr: scope.as_ptr(),
len: scope.len(),
};
let message_view = StringView {
ptr: msg.as_ptr(),
len: msg.len(),
};
unsafe { callback(self.user_data, level as u32, scope_view, message_view) }
}
None => {
let _ = writeln!(std::io::stderr(), "[polyplug] [{scope}] {msg}");
}
}
}
}
pub(crate) trait LogSink: Send + Sync {
fn emit(&self, level: LogLevel, scope: &str, message: &str);
}
impl<F> LogSink for F
where
F: Fn(LogLevel, &str, &str) + Send + Sync,
{
fn emit(&self, level: LogLevel, scope: &str, message: &str) {
self(level, scope, message)
}
}
pub(crate) struct LoggerClosure(pub(crate) Box<dyn LogSink>);
pub struct RecoveringGuard<G> {
guard: ManuallyDrop<G>,
recovered: Option<(LoggerHandle, &'static str)>,
}
impl<G> RecoveringGuard<G> {
pub fn new(
result: Result<G, PoisonError<G>>,
logger: LoggerHandle,
scope: &'static str,
) -> RecoveringGuard<G> {
match result {
Ok(guard) => RecoveringGuard {
guard: ManuallyDrop::new(guard),
recovered: None,
},
Err(poison) => RecoveringGuard {
guard: ManuallyDrop::new(poison.into_inner()),
recovered: Some((logger, scope)),
},
}
}
}
impl<G> Deref for RecoveringGuard<G> {
type Target = G;
fn deref(&self) -> &G {
&self.guard
}
}
impl<G> DerefMut for RecoveringGuard<G> {
fn deref_mut(&mut self) -> &mut G {
&mut self.guard
}
}
impl<G> Drop for RecoveringGuard<G> {
fn drop(&mut self) {
unsafe {
ManuallyDrop::drop(&mut self.guard);
}
if let Some((logger, scope)) = self.recovered.take() {
logger.log(LogLevel::Warn, scope, || {
String::from("lock poisoned, recovering")
});
}
}
}
pub trait RecoverPoisoned<G> {
fn recover_poisoned(self, logger: LoggerHandle, scope: &'static str) -> RecoveringGuard<G>;
}
impl<G> RecoverPoisoned<G> for Result<G, PoisonError<G>> {
fn recover_poisoned(self, logger: LoggerHandle, scope: &'static str) -> RecoveringGuard<G> {
RecoveringGuard::new(self, logger, scope)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use core::ffi::c_void;
use core::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, MutexGuard, TryLockError};
use polyplug_abi::types::{LogLevel, StringView};
use super::{LoggerHandle, RecoverPoisoned, RecoveringGuard};
unsafe extern "C" fn capture_log(
user_data: *mut c_void,
level: u32,
scope: StringView,
message: StringView,
) {
let sink: &Mutex<Vec<(u32, String, String)>> =
unsafe { &*(user_data as *const Mutex<Vec<(u32, String, String)>>) };
let (scope_owned, message_owned): (String, String) =
unsafe { (scope.as_str().to_owned(), message.as_str().to_owned()) };
sink.lock()
.expect("test sink lock")
.push((level, scope_owned, message_owned));
}
fn capture_handle(
sink: &Mutex<Vec<(u32, String, String)>>,
max_level: LogLevel,
) -> LoggerHandle {
LoggerHandle {
callback: Some(capture_log),
user_data: sink as *const Mutex<Vec<(u32, String, String)>> as *mut c_void,
max_level: max_level as u32,
}
}
#[test]
fn callback_receives_level_scope_message() {
let sink: Box<Mutex<Vec<(u32, String, String)>>> = Box::new(Mutex::new(Vec::new()));
let logger: LoggerHandle = capture_handle(&sink, LogLevel::Trace);
logger.log(LogLevel::Info, "store", || String::from("hello"));
let records: Vec<(u32, String, String)> = sink.lock().expect("sink").clone();
assert_eq!(
records,
vec![(
LogLevel::Info as u32,
String::from("store"),
String::from("hello")
)]
);
}
#[test]
fn max_level_filters_and_skips_formatting() {
let sink: Box<Mutex<Vec<(u32, String, String)>>> = Box::new(Mutex::new(Vec::new()));
let logger: LoggerHandle = capture_handle(&sink, LogLevel::Error);
let formatted: AtomicBool = AtomicBool::new(false);
logger.log(LogLevel::Warn, "store", || {
formatted.store(true, Ordering::SeqCst);
String::from("must not be formatted")
});
assert!(sink.lock().expect("sink").is_empty());
assert!(!formatted.load(Ordering::SeqCst));
logger.log(LogLevel::Error, "store", || String::from("boom"));
assert_eq!(sink.lock().expect("sink").len(), 1);
}
#[test]
fn null_callback_defaults_to_warn_cap() {
let logger = LoggerHandle {
callback: None,
user_data: core::ptr::null_mut(),
max_level: LogLevel::Trace as u32,
};
assert!(logger.enabled(LogLevel::Error));
assert!(logger.enabled(LogLevel::Warn));
assert!(!logger.enabled(LogLevel::Info));
assert!(!logger.enabled(LogLevel::Debug));
assert!(!logger.enabled(LogLevel::Trace));
let default_logger: LoggerHandle = LoggerHandle::default_stderr();
assert!(default_logger.enabled(LogLevel::Warn));
assert!(!default_logger.enabled(LogLevel::Info));
}
struct PoisonProbe {
lock: Mutex<u32>,
saw_released_lock: AtomicBool,
delivered: AtomicBool,
}
unsafe extern "C" fn probe_log(
user_data: *mut c_void,
level: u32,
_scope: StringView,
_message: StringView,
) {
assert_eq!(level, LogLevel::Warn as u32);
let probe: &PoisonProbe = unsafe { &*(user_data as *const PoisonProbe) };
if matches!(probe.lock.try_lock(), Err(TryLockError::Poisoned(_))) {
probe.saw_released_lock.store(true, Ordering::SeqCst);
}
probe.delivered.store(true, Ordering::SeqCst);
}
#[test]
fn recovery_log_runs_after_guard_release() {
let probe: Box<PoisonProbe> = Box::new(PoisonProbe {
lock: Mutex::new(7),
saw_released_lock: AtomicBool::new(false),
delivered: AtomicBool::new(false),
});
let probe_ref: &PoisonProbe = &probe;
let _ = std::thread::scope(|s| {
s.spawn(|| {
let _guard: MutexGuard<'_, u32> = probe_ref.lock.lock().expect("not yet poisoned");
panic!("poison the probe lock");
})
.join()
});
assert!(probe.lock.is_poisoned());
let logger = LoggerHandle {
callback: Some(probe_log),
user_data: (&*probe) as *const PoisonProbe as *mut c_void,
max_level: LogLevel::Trace as u32,
};
{
let guard: RecoveringGuard<MutexGuard<'_, u32>> =
probe.lock.lock().recover_poisoned(logger, "test");
assert_eq!(**guard, 7);
assert!(!probe.delivered.load(Ordering::SeqCst));
}
assert!(probe.delivered.load(Ordering::SeqCst));
assert!(probe.saw_released_lock.load(Ordering::SeqCst));
}
}