ethers-rs/ethers-solc/src/report.rs

424 lines
14 KiB
Rust

//! Subscribe to events in the compiler pipeline
//!
//! The _reporter_ is the component of the [`Project::compile()`] pipeline which is responsible
//! for reporting on specific steps in the process.
//!
//! By default, the current reporter is a noop that does
//! nothing.
//!
//! To use another report implementation, it must be set as the current reporter.
//! There are two methods for doing so: [`with_scoped`] and
//! [`set_global`]. `with_scoped` sets the reporter for the
//! duration of a scope, while `set_global` sets a global default report
//! for the entire process.
// https://github.com/tokio-rs/tracing/blob/master/tracing-core/src/dispatch.rs
use crate::{remappings::Remapping, CompilerInput, CompilerOutput, Solc};
use semver::Version;
use std::{
any::{Any, TypeId},
cell::RefCell,
error::Error,
fmt,
path::Path,
ptr::NonNull,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
},
};
thread_local! {
static CURRENT_STATE: State = State {
scoped: RefCell::new(Report::none()),
};
}
static EXISTS: AtomicBool = AtomicBool::new(false);
static SCOPED_COUNT: AtomicUsize = AtomicUsize::new(0);
// tracks the state of `GLOBAL_REPORTER`
static GLOBAL_REPORTER_STATE: AtomicUsize = AtomicUsize::new(UN_SET);
const UN_SET: usize = 0;
const SETTING: usize = 1;
const SET: usize = 2;
static mut GLOBAL_REPORTER: Option<Report> = None;
/// Install this `Reporter` as the global default if one is
/// not already set.
///
/// # Errors
/// Returns an Error if the initialization was unsuccessful, likely
/// because a global reporter was already installed by another
/// call to `try_init`.
pub fn try_init<T>(reporter: T) -> Result<(), Box<dyn Error + Send + Sync + 'static>>
where
T: Reporter + Send + Sync + 'static,
{
set_global_reporter(Report::new(reporter))?;
Ok(())
}
/// Install this `Reporter` as the global default.
///
/// # Panics
///
/// Panics if the initialization was unsuccessful, likely because a
/// global reporter was already installed by another call to `try_init`.
/// ```rust
/// use ethers_solc::report::BasicStdoutReporter;
/// let subscriber = ethers_solc::report::init(BasicStdoutReporter::default());
/// ```
pub fn init<T>(reporter: T)
where
T: Reporter + Send + Sync + 'static,
{
try_init(reporter).expect("Failed to install global reporter")
}
/// Trait representing the functions required to emit information about various steps in the
/// compiler pipeline.
///
/// This trait provides a series of callbacks that are invoked at certain parts of the
/// [`crate::Project::compile()`] process.
///
/// Implementers of this trait can use these callbacks to emit additional information, for example
/// print custom messages to `stdout`.
///
/// A `Reporter` is entirely passive and only listens to incoming "events".
pub trait Reporter: 'static {
/// Callback invoked right before [`Solc::compile()`] is called
fn on_solc_spawn(&self, _solc: &Solc, _version: &Version, _input: &CompilerInput) {}
/// Invoked with the `CompilerOutput` if [`Solc::compiled()`] was successful
fn on_solc_success(&self, _solc: &Solc, _version: &Version, _output: &CompilerOutput) {}
/// Invoked before a new [`Solc`] bin is installed
fn on_solc_installation_start(&self, _version: &Version) {}
/// Invoked before a new [`Solc`] bin was successfully installed
fn on_solc_installation_success(&self, _version: &Version) {}
/// Invoked if the import couldn't be resolved with these remappings
fn on_unresolved_import(&self, _import: &Path, _remappings: &[Remapping]) {}
/// If `self` is the same type as the provided `TypeId`, returns an untyped
/// [`NonNull`] pointer to that type. Otherwise, returns `None`.
///
/// If you wish to downcast a `Reporter`, it is strongly advised to use
/// the safe API provided by [`downcast_ref`] instead.
///
/// This API is required for `downcast_raw` to be a trait method; a method
/// signature like [`downcast_ref`] (with a generic type parameter) is not
/// object-safe, and thus cannot be a trait method for `Reporter`. This
/// means that if we only exposed `downcast_ref`, `Reporter`
/// implementations could not override the downcasting behavior
///
/// # Safety
///
/// The [`downcast_ref`] method expects that the pointer returned by
/// `downcast_raw` points to a valid instance of the type
/// with the provided `TypeId`. Failure to ensure this will result in
/// undefined behaviour, so implementing `downcast_raw` is unsafe.
unsafe fn downcast_raw(&self, id: TypeId) -> Option<NonNull<()>> {
if id == TypeId::of::<Self>() {
Some(NonNull::from(self).cast())
} else {
None
}
}
}
impl dyn Reporter {
/// Returns `true` if this `Reporter` is the same type as `T`.
pub fn is<T: Any>(&self) -> bool {
self.downcast_ref::<T>().is_some()
}
/// Returns some reference to this `Reporter` value if it is of type `T`,
/// or `None` if it isn't.
pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
unsafe {
let raw = self.downcast_raw(TypeId::of::<T>())?;
Some(&*(raw.cast().as_ptr()))
}
}
}
pub(crate) fn solc_spawn(solc: &Solc, version: &Version, input: &CompilerInput) {
get_default(|r| r.reporter.on_solc_spawn(solc, version, input));
}
pub(crate) fn solc_success(solc: &Solc, version: &Version, output: &CompilerOutput) {
get_default(|r| r.reporter.on_solc_success(solc, version, output));
}
#[allow(unused)]
pub(crate) fn solc_installation_start(version: &Version) {
get_default(|r| r.reporter.on_solc_installation_start(version));
}
#[allow(unused)]
pub(crate) fn solc_installation_success(version: &Version) {
get_default(|r| r.reporter.on_solc_installation_success(version));
}
pub(crate) fn unresolved_import(import: &Path, remappings: &[Remapping]) {
get_default(|r| r.reporter.on_unresolved_import(import, remappings));
}
fn get_global() -> Option<&'static Report> {
if GLOBAL_REPORTER_STATE.load(Ordering::SeqCst) != SET {
return None
}
unsafe {
// This is safe given the invariant that setting the global reporter
// also sets `GLOBAL_REPORTER_STATE` to `SET`.
Some(GLOBAL_REPORTER.as_ref().expect(
"Reporter invariant violated: GLOBAL_REPORTER must be initialized before GLOBAL_REPORTER_STATE is set",
))
}
}
/// Executes a closure with a reference to this thread's current [reporter].
#[inline(always)]
pub fn get_default<T, F>(mut f: F) -> T
where
F: FnMut(&Report) -> T,
{
if SCOPED_COUNT.load(Ordering::Acquire) == 0 {
// fast path if no scoped reporter has been set; use the global
// default.
return if let Some(glob) = get_global() { f(glob) } else { f(&Report::none()) }
}
get_default_scoped(f)
}
#[inline(never)]
fn get_default_scoped<T, F>(mut f: F) -> T
where
F: FnMut(&Report) -> T,
{
CURRENT_STATE
.try_with(|state| {
let scoped = state.scoped.borrow_mut();
f(&*scoped)
})
.unwrap_or_else(|_| f(&Report::none()))
}
/// Executes a closure with a reference to the `Reporter`.
pub fn with_global<T>(f: impl FnOnce(&Report) -> T) -> Option<T> {
let report = get_global()?;
Some(f(report))
}
/// Sets this reporter as the scoped reporter for the duration of a closure.
pub fn with_scoped<T>(report: &Report, f: impl FnOnce() -> T) -> T {
// When this guard is dropped, the scoped reporter will be reset to the
// prior reporter. Using this (rather than simply resetting after calling
// `f`) ensures that we always reset to the prior reporter even if `f`
// panics.
let _guard = set_scoped(report);
f()
}
/// The report state of a thread.
struct State {
/// This thread's current scoped reporter.
scoped: RefCell<Report>,
}
impl State {
/// Replaces the current scoped reporter on this thread with the provided
/// reporter.
///
/// Dropping the returned `ResetGuard` will reset the scoped reporter to
/// the previous value.
#[inline]
fn set_scoped(new_report: Report) -> ScopeGuard {
let prior = CURRENT_STATE.try_with(|state| state.scoped.replace(new_report)).ok();
EXISTS.store(true, Ordering::Release);
SCOPED_COUNT.fetch_add(1, Ordering::Release);
ScopeGuard(prior)
}
}
/// A guard that resets the current scoped reporter to the prior
/// scoped reporter when dropped.
#[derive(Debug)]
pub struct ScopeGuard(Option<Report>);
impl Drop for ScopeGuard {
#[inline]
fn drop(&mut self) {
SCOPED_COUNT.fetch_sub(1, Ordering::Release);
if let Some(report) = self.0.take() {
// Replace the reporter and then drop the old one outside
// of the thread-local context.
let prev = CURRENT_STATE.try_with(|state| state.scoped.replace(report));
drop(prev)
}
}
}
/// Sets the reporter as the scoped reporter for the duration of the lifetime
/// of the returned DefaultGuard
#[must_use = "Dropping the guard unregisters the reporter."]
pub fn set_scoped(reporter: &Report) -> ScopeGuard {
// When this guard is dropped, the scoped reporter will be reset to the
// prior default. Using this ensures that we always reset to the prior
// reporter even if the thread calling this function panics.
State::set_scoped(reporter.clone())
}
/// A no-op [`Reporter`] that does nothing.
#[derive(Copy, Clone, Debug, Default)]
pub struct NoReporter(());
impl Reporter for NoReporter {}
/// A [`Reporter`] that emits some general information to `stdout`
#[derive(Copy, Clone, Debug, Default)]
pub struct BasicStdoutReporter(());
impl Reporter for BasicStdoutReporter {
/// Callback invoked right before [`Solc::compile()`] is called
fn on_solc_spawn(&self, _solc: &Solc, version: &Version, input: &CompilerInput) {
println!(
"Compiling {} files with {}.{}.{}",
input.sources.len(),
version.major,
version.minor,
version.patch
);
}
/// Invoked before a new [`Solc`] bin is installed
fn on_solc_installation_start(&self, version: &Version) {
println!("installing solc version \"{}\"", version);
}
/// Invoked before a new [`Solc`] bin was successfully installed
fn on_solc_installation_success(&self, version: &Version) {
println!("Successfully installed solc {}", version);
}
fn on_unresolved_import(&self, import: &Path, remappings: &[Remapping]) {
println!(
"Unable to resolve import: \"{}\" with remappings:\n {}",
import.display(),
remappings.iter().map(|r| r.to_string()).collect::<Vec<_>>().join("\n ")
);
}
}
/// Returned if setting the global reporter fails.
#[derive(Debug)]
pub struct SetGlobalReporterError {
// private marker so this type can't be initiated
_priv: (),
}
impl fmt::Display for SetGlobalReporterError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad("a global reporter has already been set")
}
}
impl Error for SetGlobalReporterError {}
/// `Report` trace data to a [`Reporter`].
#[derive(Clone)]
pub struct Report {
reporter: Arc<dyn Reporter + Send + Sync>,
}
impl Report {
/// Returns a new `Report` that does nothing
pub fn none() -> Self {
Report { reporter: Arc::new(NoReporter::default()) }
}
/// Returns a `Report` that forwards to the given [`Reporter`].
///
/// [`Reporter`]: ../reporter/trait.Reporter.html
pub fn new<S>(reporter: S) -> Self
where
S: Reporter + Send + Sync + 'static,
{
Self { reporter: Arc::new(reporter) }
}
/// Returns `true` if this `Report` forwards to a reporter of type
/// `T`.
#[inline]
pub fn is<T: Any>(&self) -> bool {
<dyn Reporter>::is::<T>(&*self.reporter)
}
}
impl fmt::Debug for Report {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad("Report(...)")
}
}
/// Sets this report as the global default for the duration of the entire program.
///
/// The global reporter can only be set once; additional attempts to set the global reporter will
/// fail. Returns `Err` if the global reporter has already been set.
fn set_global_reporter(report: Report) -> Result<(), SetGlobalReporterError> {
// `compare_exchange` tries to store `SETTING` if the current value is `UN_SET`
// this returns `Ok(_)` if the current value of `GLOBAL_REPORTER_STATE` was `UN_SET` and
// `SETTING` was written, this guarantees the value is `SETTING`.
if GLOBAL_REPORTER_STATE
.compare_exchange(UN_SET, SETTING, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
unsafe {
GLOBAL_REPORTER = Some(report);
}
GLOBAL_REPORTER_STATE.store(SET, Ordering::SeqCst);
Ok(())
} else {
Err(SetGlobalReporterError { _priv: () })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scoped_reporter_works() {
struct TestReporter;
impl Reporter for TestReporter {}
with_scoped(&Report::new(TestReporter), || {
get_default(|reporter| assert!(reporter.is::<TestReporter>()))
});
}
#[test]
fn global_and_scoped_reporter_works() {
get_default(|reporter| {
assert!(reporter.is::<NoReporter>());
});
set_global_reporter(Report::new(BasicStdoutReporter::default())).unwrap();
struct TestReporter;
impl Reporter for TestReporter {}
with_scoped(&Report::new(TestReporter), || {
get_default(|reporter| assert!(reporter.is::<TestReporter>()))
});
get_default(|reporter| assert!(reporter.is::<BasicStdoutReporter>()))
}
}