//! Cross-platform resource cleanup helpers. use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::Mutex; use std::time::Duration; use libremetaverse_types::compat::{CancellationTokenSource, Close, ExternalError, Task, Thread}; use crate::Error; type ErrorLogger = Box) + Send + Sync>; fn panic_error(payload: Box) -> ExternalError { if let Some(message) = payload.downcast_ref::<&str>() { ExternalError((*message).to_owned()) } else if let Some(message) = payload.downcast_ref::() { ExternalError(message.clone()) } else { ExternalError("action panicked with a non-string payload".to_owned()) } } fn report(logger: &Option, message: impl Into, error: Option) { if let Some(logger) = logger { logger(message.into(), error); } } /// Helpers that contain cleanup failures and report them through a caller-owned /// logger, matching the upstream disposal contract. pub struct DisposalHelper; impl DisposalHelper { pub fn safe_action( action: Box, action_name: Option, logger: Option, ) -> Result<(), Error> { if let Err(payload) = catch_unwind(AssertUnwindSafe(action)) { let message = action_name.filter(|name| !name.is_empty()).map_or_else( || "Error executing action".to_owned(), |name| format!("Error executing {name}"), ); report(&logger, message, Some(panic_error(payload))); } Ok(()) } pub fn safe_cancel_and_dispose( mut source: Option, logger: Option, ) -> Result<(), Error> { let Some(source) = source.as_mut() else { return Ok(()); }; source.cancel(); if let Err(error) = source.close() { report( &logger, "Error disposing CancellationTokenSource", Some(error), ); } Ok(()) } pub fn safe_dispose( mut resource: Box, resource_name: Option, logger: Option, ) -> Result<(), Error> { if let Err(error) = resource.close() { let message = resource_name.filter(|name| !name.is_empty()).map_or_else( || "Error disposing resource".to_owned(), |name| format!("Error disposing {name}"), ); report(&logger, message, Some(error)); } Ok(()) } pub fn safe_dispose_all( resources: Box>>, logger: Option, ) -> Result<(), Error> { for mut resource in resources { if let Err(error) = resource.close() { report(&logger, "Error disposing resource", Some(error)); } } Ok(()) } pub fn safe_dispose_clear( mut collection: Vec, logger: Option, ) -> Result<(), Error> { for resource in &mut collection { if let Err(error) = resource.close() { report(&logger, "Error disposing resource", Some(error)); } } collection.clear(); Ok(()) } pub fn safe_join_thread( thread: Thread, timeout: Duration, logger: Option, ) -> Result { if !thread.is_alive() { return Ok(true); } match thread.join_timeout(timeout) { Ok(true) => Ok(true), Ok(false) => { report( &logger, format!( "Thread {} did not exit in time", thread.name().unwrap_or("unnamed") ), None, ); Ok(false) } Err(error) => { report( &logger, format!( "Error waiting for thread {}", thread.name().unwrap_or("unnamed") ), Some(error), ); Ok(false) } } } pub fn safe_wait_task_with_task_time_span_action( task: Task<()>, timeout: Duration, logger: Option, ) -> Result { match task.wait_timeout(timeout) { Ok(Some(())) => Ok(true), Ok(None) => { report(&logger, "Task did not complete in time", None); Ok(false) } Err(error) => { report( &logger, "Error waiting for task", Some(ExternalError(error.to_string())), ); Ok(false) } } } pub async fn safe_wait_task_with_task_time_span_action_b4795938( task: Task<()>, timeout: Duration, logger: Option, ) -> Result { // `Task` is executor-neutral; its bounded driver parks only this worker // thread and is woken by the underlying future when progress is ready. Self::safe_wait_task_with_task_time_span_action(task, timeout, logger) } pub async fn using_with_func_func( factory: Box TDisposable + Send + Sync>, action: Box Task> + Send + Sync>, ) -> Result, Error> { let mut guard = CloseGuard::new(factory()); let result = action(guard.resource_mut()).await; guard.close()?; result } pub fn using_with_func_func_3fbb9598( factory: Box TDisposable + Send + Sync>, action: Box Option + Send + Sync>, ) -> Result, Error> { let mut guard = CloseGuard::new(factory()); let result = action(guard.resource_mut()); guard.close()?; Ok(result) } } struct CloseGuard { resource: Option, } impl CloseGuard { fn new(resource: T) -> Self { Self { resource: Some(resource), } } fn resource_mut(&mut self) -> &mut T { self.resource .as_mut() .expect("resource remains until close") } fn close(&mut self) -> Result<(), Error> { let mut resource = self.resource.take().ok_or(Error::InvalidOperation)?; resource.close().map_err(|_| Error::InvalidOperation) } } impl Drop for CloseGuard { fn drop(&mut self) { if let Some(resource) = self.resource.as_mut() { let _ = resource.close(); } } } /// Runs one callback exactly once when explicitly disposed or dropped. pub struct DisposalHelperDisposalGuard { on_dispose: Mutex>>, } impl DisposalHelperDisposalGuard { pub fn new(on_dispose: Box) -> Result { Ok(Self { on_dispose: Mutex::new(Some(on_dispose)), }) } pub fn dispose(&self) -> Result<(), Error> { let callback = self .on_dispose .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .take(); let Some(callback) = callback else { return Ok(()); }; catch_unwind(AssertUnwindSafe(callback)).map_err(|_| Error::InvalidOperation) } } impl Close for DisposalHelperDisposalGuard { fn close(&mut self) -> Result<(), ExternalError> { self.dispose() .map_err(|error| ExternalError(error.to_string())) } } impl Drop for DisposalHelperDisposalGuard { fn drop(&mut self) { let _ = self.dispose(); } } #[cfg(test)] mod tests { use super::*; use std::sync::{Arc, atomic::AtomicUsize}; struct CountedClose(Arc); impl Close for CountedClose { fn close(&mut self) -> Result<(), ExternalError> { self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed); Ok(()) } } #[test] fn disposal_guard_runs_callback_once() { let count = Arc::new(AtomicUsize::new(0)); let observed = Arc::clone(&count); let guard = DisposalHelperDisposalGuard::new(Box::new(move || { observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed); })) .unwrap(); guard.dispose().unwrap(); guard.dispose().unwrap(); drop(guard); assert_eq!(count.load(std::sync::atomic::Ordering::Relaxed), 1); } #[test] fn using_closes_after_action() { let count = Arc::new(AtomicUsize::new(0)); let factory_count = Arc::clone(&count); let result = DisposalHelper::using_with_func_func_3fbb9598( Box::new(move || CountedClose(factory_count.clone())), Box::new(|_| Some(42)), ) .unwrap(); assert_eq!(result, Some(42)); assert_eq!(count.load(std::sync::atomic::Ordering::Relaxed), 1); } #[test] fn bounded_thread_join_reports_timeout_then_completion() { let thread = Thread::spawn(Some("disposal-test".to_owned()), || { std::thread::sleep(Duration::from_millis(30)); }) .unwrap(); assert!( !DisposalHelper::safe_join_thread(thread.clone(), Duration::from_millis(1), None,) .unwrap() ); assert!(DisposalHelper::safe_join_thread(thread, Duration::from_secs(1), None,).unwrap()); } }