#![feature(ptr_metadata)]
use std::io::{self, Write};
use std::mem::replace;
use std::num::NonZeroU32;
use std::panic::Location;
use std::path::Path;
use std::sync::Mutex;
use std::thread::{self, Thread, ThreadId};
use std::{fmt, panic, ptr};
pub use log;
use log::{Level, LevelFilter, Log, set_logger, set_max_level};
use sys_abstract::SystemImpl;
use time::{Date, OffsetDateTime};
mod sys_abstract;
#[cfg(target_arch = "wasm32")]
mod sys_web;
#[cfg(target_arch = "wasm32")]
use sys_web::System;
#[cfg(not(target_arch = "wasm32"))]
mod sys_native;
#[cfg(not(target_arch = "wasm32"))]
use sys_native::System;
pub struct Settings {
pub title: &'static str,
pub filters: &'static [(&'static str, LevelFilter)],
pub file_out: Option<&'static Path>,
pub console_out: bool,
pub panic_hook: Option<fn(Panic<'_>)>,
}
impl Settings {
pub fn init(self) {
let Self {
title,
filters,
file_out,
console_out,
panic_hook,
} = self;
let max_level = filters
.iter()
.map(|&(_, level)| level)
.max()
.unwrap_or(LevelFilter::Off);
if let Some(handler) = panic_hook {
panic::set_hook(Box::new(panic_handler(handler)));
}
let date = now().date();
let logger = Logger {
title,
filters,
file_out: file_out.map(System::file_new),
console_out: console_out.then(System::console_new),
prev_day: Mutex::new(date.to_julian_day()),
};
let message = Header { title, date };
if let Some(out) = &logger.file_out {
System::file_p_header(out, &message);
}
if let Some(out) = &logger.console_out {
System::console_p_header(out, &message);
}
set_logger(upcast_log(Box::leak(Box::new(logger)))).expect("Failed to apply logger");
set_max_level(max_level);
}
}
fn as_dyn_ref(logger: *const Logger) -> *const dyn Log {
logger as *const dyn Log
}
fn upcast_log(logger: &'static Logger) -> &'static dyn Log {
unsafe { &*as_dyn_ref(logger) }
}
fn downcast_log(log: &'static dyn Log) -> Option<&'static Logger> {
let (logger_ptr, logger_meta) = (&raw const *log).to_raw_parts();
let (_, fake_logger_meta) = as_dyn_ref(ptr::null::<Logger>()).to_raw_parts();
(logger_meta == fake_logger_meta).then(|| {
unsafe { &*logger_ptr.cast::<Logger>() }
})
}
struct Logger {
title: &'static str,
filters: &'static [(&'static str, LevelFilter)],
file_out: Option<<System as SystemImpl>::File>,
console_out: Option<<System as SystemImpl>::Console>,
prev_day: Mutex<i32>,
}
impl Log for Logger {
fn enabled(&self, meta: &log::Metadata) -> bool {
for (name, level) in self.filters {
if meta.target().starts_with(name) {
return *level >= meta.level();
}
}
false
}
fn log(&self, record: &log::Record) {
if self.enabled(record.metadata()) {
let now = now();
let date = now.date();
let day = date.to_julian_day();
let date = match self.prev_day.lock() {
Ok(mut lock) => (replace(&mut *lock, day) != day).then_some(date),
Err(_) => None,
};
let thread = thread::current();
let message = Record {
date,
module: record.module_path().unwrap_or("?"),
line: NonZeroU32::new(record.line().unwrap_or(0)),
thread: ThreadName::new(&thread),
args: *record.args(),
hmsms: now.time().as_hms_milli(),
level: record.level(),
};
if let Some(out) = &self.file_out {
System::file_p_record(out, &message);
}
if let Some(out) = &self.console_out {
System::console_p_record(out, &message);
}
}
}
fn flush(&self) {
self.file_out.as_ref().map(System::file_flush);
self.console_out.as_ref().map(System::console_flush);
}
}
fn now() -> OffsetDateTime {
OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc())
}
#[derive(Debug)]
pub enum ThreadName<'data> {
Name(&'data str),
Id(ThreadId),
}
impl<'data> ThreadName<'data> {
fn new(thread: &'data Thread) -> Self {
if let Some(name) = thread.name() {
Self::Name(name)
} else {
Self::Id(thread.id())
}
}
}
impl fmt::Display for ThreadName<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ThreadName::Name(name) => write!(f, "Thread {name:?}"),
ThreadName::Id(id) => write!(f, "{id:?}"),
}
}
}
struct Header {
title: &'static str,
date: Date,
}
struct Record<'data> {
date: Option<Date>,
module: &'data str,
line: Option<NonZeroU32>,
thread: ThreadName<'data>,
args: fmt::Arguments<'data>,
hmsms: (u8, u8, u8, u16),
level: Level,
}
#[derive(Debug)]
pub struct Backtrace {
#[cfg(feature = "backtrace")]
data: <System as SystemImpl>::Backtrace,
#[cfg(not(feature = "backtrace"))]
data: (),
}
impl Backtrace {
fn capture() -> Self {
Self {
data: System::backtrace_new(),
}
}
pub fn write<W: Write>(&self, writer: W) -> io::Result<()> {
System::backtrace_write(&self.data, writer)
}
pub fn as_string(&self) -> String { System::backtrace_string(&self.data) }
}
pub struct Panic<'data> {
pub thread: ThreadName<'data>,
pub message: Option<&'data str>,
pub location: Option<Location<'data>>,
pub title: &'data str,
pub path: Option<&'data Path>,
pub trace: Backtrace,
}
impl fmt::Debug for Panic<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Panic")
.field("thread", &self.thread)
.field("message", &self.message)
.field("location", &self.location)
.field("title", &self.title)
.field("path", &self.path)
.finish_non_exhaustive()
}
}
impl Panic<'_> {
fn message_str(&self) -> &str { self.message.unwrap_or("[non-string message]") }
fn location_display(&self) -> &dyn fmt::Display {
self.location.as_ref().map_or(&"[citation needed]", |v| v)
}
}
fn panic_handler(handler: fn(Panic)) -> impl Fn(&panic::PanicHookInfo) {
move |info: &panic::PanicHookInfo| {
let logger = downcast_log(log::logger());
let thread = thread::current();
let mut message = Panic {
thread: ThreadName::new(&thread),
message: info.payload_as_str(),
location: info.location().copied(),
title: "[unknown?]",
path: None,
trace: Backtrace::capture(),
};
if let Some(logger) = logger {
message.title = logger.title;
message.path = System::file_path(logger.file_out.as_ref());
if let Some(out) = &logger.file_out {
System::file_p_panic(out, &message);
}
if let Some(out) = &logger.console_out {
System::console_p_panic(out, &message);
}
handler(message);
} else {
System::fallback_p_panic(&message);
}
}
}