#[cfg(test)]
#[path = "tests/spew.rs"]
mod tests;
use crate::server::Server;
use sdk_raw::tier0::{SpewApi, SpewColor, SpewOutputFn, SpewRetval, SpewType, spew_api};
use sdk_raw::util::stub::JumpStub;
use std::cell::Cell;
use std::ffi::{CStr, c_char, c_void};
use std::io;
use std::marker::PhantomData;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::ptr::{self, NonNull};
use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
const SETTLE: Duration = Duration::from_millis(100);
const STOP_WAIT: Duration = Duration::from_secs(1);
static API: AtomicPtr<SpewApi> = AtomicPtr::new(ptr::null_mut());
static CALLBACK: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut());
static PREVIOUS: AtomicPtr<c_void> = AtomicPtr::new(ptr::null_mut());
static RUNNING: AtomicUsize = AtomicUsize::new(0);
static WARNING_FILTER: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut());
static WATCHING: AtomicBool = AtomicBool::new(false);
thread_local! {
static IN_CALLBACK: Cell<bool> = const { Cell::new(false) };
}
pub type SpewFn = fn(spew: &Spew<'_>);
pub type WarningFilterFn = fn(spew: &Spew<'_>) -> bool;
struct Running;
impl Drop for Running {
fn drop(&mut self) {
RUNNING.fetch_sub(1, Ordering::SeqCst);
}
}
#[derive(Debug, Clone, Copy)]
pub struct Spew<'a> {
kind: SpewKind,
text: &'a CStr,
group: &'a CStr,
level: i32,
color: Option<SpewColor>,
}
impl<'a> Spew<'a> {
pub fn color(&self) -> Option<SpewColor> {
self.color
}
pub fn group(&self) -> &'a CStr {
self.group
}
pub fn kind(&self) -> SpewKind {
self.kind
}
pub fn level(&self) -> i32 {
self.level
}
pub fn text(&self) -> &'a CStr {
self.text
}
}
#[doc(alias("SpewType_t"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SpewKind {
Message,
Warning,
Assert,
Error,
Log,
Other(i32),
}
impl From<SpewType> for SpewKind {
fn from(kind: SpewType) -> Self {
match kind {
SpewType::MESSAGE => Self::Message,
SpewType::WARNING => Self::Warning,
SpewType::ASSERT => Self::Assert,
SpewType::ERROR => Self::Error,
SpewType::LOG => Self::Log,
SpewType(other) => Self::Other(other),
}
}
}
#[must_use = "dropping the watch stops it"]
#[derive(Debug)]
pub struct SpewWatch {
api: SpewApi,
stub: JumpStub,
previous: Option<SpewOutputFn>,
ended: Option<Stopped>,
finished: bool,
_main_thread: PhantomData<*const ()>,
}
impl SpewWatch {
fn end(&mut self) -> Stopped {
let ended = match self.ended {
Some(ended) => ended,
None => {
let ended = self.leave();
self.ended = Some(ended);
ended
}
};
if self.finished {
return ended;
}
let start = Instant::now();
loop {
while RUNNING.load(Ordering::SeqCst) != 0 {
if start.elapsed() >= STOP_WAIT {
return Stopped::Busy;
}
std::thread::yield_now();
}
std::thread::sleep(SETTLE);
if RUNNING.load(Ordering::SeqCst) == 0 {
break;
}
}
self.finished = true;
WATCHING.store(false, Ordering::SeqCst);
ended
}
pub fn filter_warnings(&mut self, filter: Option<WarningFilterFn>) {
if self.ended.is_none() {
WARNING_FILTER.store(
filter.map_or(ptr::null_mut(), |filter| filter as *mut ()),
Ordering::SeqCst,
);
}
}
fn leave(&mut self) -> Stopped {
let api = self.api;
self.stub
.retarget(function_address(previous_or_default(api, self.previous)));
let stopped = if unsafe { (api.output)() }
.is_some_and(|current| function_address(current) == self.stub.entry())
{
unsafe { (api.set_output)(self.previous) };
Stopped::Restored
} else {
Stopped::Bypassed
};
WARNING_FILTER.store(ptr::null_mut(), Ordering::SeqCst);
CALLBACK.store(ptr::null_mut(), Ordering::SeqCst);
stopped
}
pub fn stop(&mut self) -> Stopped {
self.end()
}
}
impl Drop for SpewWatch {
fn drop(&mut self) {
if !self.finished && self.end() == Stopped::Busy {
WATCHING.store(false, Ordering::SeqCst);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Stopped {
Restored,
Bypassed,
Busy,
}
#[derive(Debug, thiserror::Error)]
pub enum WatchError {
#[error("tier0's spew functions were not found")]
Unavailable,
#[error("console output is already being watched")]
AlreadyWatching,
#[error("the jump stub could not be allocated: {0}")]
Stub(#[from] io::Error),
}
fn current_api() -> Option<SpewApi> {
let api = API.load(Ordering::SeqCst);
(!api.is_null()).then(|| unsafe { *api })
}
fn function_address(function: SpewOutputFn) -> NonNull<c_void> {
NonNull::new(function as *mut c_void).expect("functions have addresses")
}
unsafe fn notify(kind: SpewType, message: *const c_char) -> bool {
let callback = CALLBACK.load(Ordering::SeqCst);
if callback.is_null() || message.is_null() {
return true;
}
if IN_CALLBACK.try_with(|inside| inside.replace(true)) != Ok(false) {
return true;
}
let callback = unsafe { std::mem::transmute::<*mut (), SpewFn>(callback) };
let text = unsafe { CStr::from_ptr(message) };
let (group, level, color) = match current_api() {
Some(api) => unsafe {
let group = (api.group)();
let color = (api.color)();
(
if group.is_null() {
c""
} else {
CStr::from_ptr(group)
},
(api.level)(),
color.as_ref().copied(),
)
},
None => (c"", 0, None),
};
let spew = Spew {
kind: kind.into(),
text,
group,
level,
color,
};
let forward = catch_unwind(AssertUnwindSafe(|| {
callback(&spew);
if kind != SpewType::WARNING {
return true;
}
let filter = WARNING_FILTER.load(Ordering::SeqCst);
if filter.is_null() {
return true;
}
let filter = unsafe { std::mem::transmute::<*mut (), WarningFilterFn>(filter) };
filter(&spew)
}))
.unwrap_or(true);
IN_CALLBACK.try_with(|inside| inside.set(false)).ok();
forward
}
unsafe extern "C" fn output(kind: SpewType, message: *const c_char) -> SpewRetval {
RUNNING.fetch_add(1, Ordering::SeqCst);
let _running = Running;
if !unsafe { notify(kind, message) } {
return SpewRetval::CONTINUE;
}
let Some(api) = current_api() else {
return SpewRetval::CONTINUE;
};
let previous = PREVIOUS.load(Ordering::SeqCst);
let previous = if previous.is_null() {
api.default
} else {
unsafe { std::mem::transmute::<*mut c_void, SpewOutputFn>(previous) }
};
unsafe { previous(kind, message) }
}
fn previous_or_default(api: SpewApi, previous: Option<SpewOutputFn>) -> SpewOutputFn {
previous.unwrap_or(api.default)
}
pub fn watch(server: Server<'_>, callback: SpewFn) -> Result<SpewWatch, WatchError> {
let _ = server;
unsafe { watch_with(spew_api().ok_or(WatchError::Unavailable)?, callback) }
}
unsafe fn watch_with(api: SpewApi, callback: SpewFn) -> Result<SpewWatch, WatchError> {
if WATCHING.swap(true, Ordering::SeqCst) {
return Err(WatchError::AlreadyWatching);
}
let stub = match JumpStub::new(function_address(output)) {
Ok(stub) => stub,
Err(error) => {
WATCHING.store(false, Ordering::SeqCst);
return Err(error.into());
}
};
let previous = unsafe { (api.output)() };
PREVIOUS.store(
previous.map_or(ptr::null_mut(), |function| {
function_address(function).as_ptr()
}),
Ordering::SeqCst,
);
API.store(Box::into_raw(Box::new(api)), Ordering::SeqCst);
WARNING_FILTER.store(ptr::null_mut(), Ordering::SeqCst);
CALLBACK.store(callback as *mut (), Ordering::SeqCst);
unsafe {
(api.set_output)(Some(std::mem::transmute::<*mut c_void, SpewOutputFn>(
stub.entry().as_ptr(),
)));
}
Ok(SpewWatch {
api,
stub,
previous,
ended: None,
finished: false,
_main_thread: PhantomData,
})
}