diff --git a/.changeset/ten-spoons-study.md b/.changeset/ten-spoons-study.md new file mode 100644 index 0000000000..13f3d1a201 --- /dev/null +++ b/.changeset/ten-spoons-study.md @@ -0,0 +1,5 @@ +--- +"@nomicfoundation/edr": patch +--- + +Fix node.js runtime freezing on shutdown diff --git a/Cargo.lock b/Cargo.lock index 6d3252f4b8..64ea28fe65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2052,9 +2052,9 @@ dependencies = [ [[package]] name = "napi" -version = "2.14.2" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc1cb00cde484640da9f01a124edbb013576a6ae9843b23857c940936b76d91" +checksum = "54a63d0570e4c3e0daf7a8d380563610e159f538e20448d6c911337246f40e84" dependencies = [ "anyhow", "bitflags 2.4.2", @@ -2075,9 +2075,9 @@ checksum = "882a73d9ef23e8dc2ebbffb6a6ae2ef467c0f18ac10711e4cc59c5485d41df0e" [[package]] name = "napi-derive" -version = "2.14.6" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e61bec1ee990ae3e9a5f443484c65fb38e571a898437f0ad283ed69c82fc59c0" +checksum = "05bb7c37e3c1dda9312fdbe4a9fc7507fca72288ba154ec093e2d49114e727ce" dependencies = [ "cfg-if", "convert_case 0.6.0", @@ -2089,9 +2089,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "1.0.58" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2314f777bc9cde51705d991c44466cee4de4a3f41c6d3d019fcbbebb5cdd47c4" +checksum = "f785a8b8d7b83e925f5aa6d2ae3c159d17fe137ac368dc185bef410e7acdaeb4" dependencies = [ "convert_case 0.6.0", "once_cell", diff --git a/crates/edr_napi/Cargo.toml b/crates/edr_napi/Cargo.toml index dec97fc16c..7be4449931 100644 --- a/crates/edr_napi/Cargo.toml +++ b/crates/edr_napi/Cargo.toml @@ -14,8 +14,8 @@ k256 = { version = "0.13.1", default-features = false, features = ["arithmetic", log = { version = "0.4.20", default-features = false } # when napi is pinned, be sure to pin napi-derive to the same version # The `async` feature ensures that a tokio runtime is available -napi = { version = "2.12.4", default-features = false, features = ["async", "error_anyhow", "napi8", "serde-json"] } -napi-derive = "2.12.3" +napi = { version = "2.16.0", default-features = false, features = ["async", "error_anyhow", "napi8", "serde-json"] } +napi-derive = "2.16.0" edr_defaults = { version = "0.2.0-dev", path = "../edr_defaults" } edr_evm = { version = "0.2.0-dev", path = "../edr_evm", features = ["tracing"]} edr_eth = { version = "0.2.0-dev", path = "../edr_eth" } diff --git a/crates/edr_napi/package.json b/crates/edr_napi/package.json index 5900fb79e7..abe0f3e941 100644 --- a/crates/edr_napi/package.json +++ b/crates/edr_napi/package.json @@ -52,8 +52,8 @@ "universal": "napi universal", "version": "napi version", "pretest": "pnpm build", - "test": "pnpm tsc && mocha --recursive \"test/**/*.ts\" --exit", - "testNoBuild": "pnpm tsc && mocha --recursive \"test/**/*.ts\" --exit", + "test": "pnpm tsc && mocha --recursive \"test/**/*.ts\"", + "testNoBuild": "pnpm tsc && mocha --recursive \"test/**/*.ts\"", "clean": "rm -rf @nomicfoundation/edr.node" } } diff --git a/crates/edr_napi/src/call_override.rs b/crates/edr_napi/src/call_override.rs index dd598fdea6..0544b0bf4b 100644 --- a/crates/edr_napi/src/call_override.rs +++ b/crates/edr_napi/src/call_override.rs @@ -1,14 +1,16 @@ -use std::sync::mpsc::{channel, Sender}; +use std::sync::mpsc::channel; use edr_eth::{Address, Bytes}; -use napi::{bindgen_prelude::Buffer, Env, JsFunction, NapiRaw, Status}; +use napi::{ + bindgen_prelude::Buffer, + threadsafe_function::{ + ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, + }, + Env, JsFunction, Status, +}; use napi_derive::napi; -use crate::{ - cast::TryCast, - sync::{await_promise, handle_error}, - threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode}, -}; +use crate::cast::TryCast; /// The result of executing a call override. #[napi(object)] @@ -34,20 +36,16 @@ impl TryCast> for Option>>, } #[derive(Clone)] pub struct CallOverrideCallback { - call_override_callback_fn: ThreadsafeFunction, + call_override_callback_fn: ThreadsafeFunction, } impl CallOverrideCallback { pub fn new(env: &Env, call_override_callback: JsFunction) -> napi::Result { - let call_override_callback_fn = ThreadsafeFunction::create( - env.raw(), - // SAFETY: The callback is guaranteed to be valid for the lifetime of the inspector. - unsafe { call_override_callback.raw() }, + let mut call_override_callback_fn = call_override_callback.create_threadsafe_function( 0, |ctx: ThreadSafeCallContext| { let address = ctx @@ -60,17 +58,14 @@ impl CallOverrideCallback { .create_buffer_with_data(ctx.value.data.to_vec())? .into_raw(); - let sender = ctx.value.sender.clone(); - let promise = ctx.callback.call(None, &[address, data])?; - let result = await_promise::< - Option, - Option, - >(ctx.env, promise, ctx.value.sender); - - handle_error(sender, result) + Ok(vec![address, data]) }, )?; + // Maintain a weak reference to the function to avoid the event loop from + // exiting. + call_override_callback_fn.unref(env)?; + Ok(Self { call_override_callback_fn, }) @@ -83,13 +78,22 @@ impl CallOverrideCallback { ) -> Option { let (sender, receiver) = channel(); - let status = self.call_override_callback_fn.call( + let status = self.call_override_callback_fn.call_with_return_value( CallOverrideCall { contract_address, data, - sender, }, ThreadsafeFunctionCallMode::Blocking, + move |result: Option| { + let result = result.try_cast(); + + sender.send(result).map_err(|_error| { + napi::Error::new( + Status::GenericFailure, + "Failed to send result from call_override_callback", + ) + }) + }, ); assert_eq!(status, Status::Ok, "Call override callback failed"); diff --git a/crates/edr_napi/src/lib.rs b/crates/edr_napi/src/lib.rs index 69e8e0e079..d5c6d98eb5 100644 --- a/crates/edr_napi/src/lib.rs +++ b/crates/edr_napi/src/lib.rs @@ -16,7 +16,5 @@ mod result; #[cfg(feature = "scenarios")] mod scenarios; mod subscribe; -mod sync; -mod threadsafe_function; mod trace; mod withdrawal; diff --git a/crates/edr_napi/src/log.rs b/crates/edr_napi/src/log.rs index 1ca7395073..2137667c47 100644 --- a/crates/edr_napi/src/log.rs +++ b/crates/edr_napi/src/log.rs @@ -1,5 +1,3 @@ -use std::mem; - use napi::{bindgen_prelude::Buffer, Env, JsBuffer, JsBufferValue}; use napi_derive::napi; @@ -19,18 +17,9 @@ impl ExecutionLog { .map(|topic| Buffer::from(topic.as_slice())) .collect(); - let data = log.data.data.clone(); - let data = unsafe { - env.create_buffer_with_borrowed_data( - data.as_ptr(), - data.len(), - data, - |data: edr_eth::Bytes, _env| { - mem::drop(data); - }, - ) - } - .map(JsBufferValue::into_raw)?; + let data = env + .create_buffer_with_data(log.data.data.to_vec()) + .map(JsBufferValue::into_raw)?; Ok(Self { address: Buffer::from(log.address.as_slice()), diff --git a/crates/edr_napi/src/logger.rs b/crates/edr_napi/src/logger.rs index 1e0b64016b..a8b46216aa 100644 --- a/crates/edr_napi/src/logger.rs +++ b/crates/edr_napi/src/logger.rs @@ -1,7 +1,4 @@ -use std::{ - fmt::Display, - sync::mpsc::{channel, Sender}, -}; +use std::{fmt::Display, sync::mpsc::channel}; use ansi_term::{Color, Style}; use edr_eth::{Bytes, B256, U256}; @@ -13,14 +10,15 @@ use edr_evm::{ }; use edr_provider::{ProviderError, TransactionFailure}; use itertools::izip; -use napi::{Env, JsFunction, NapiRaw, Status}; +use napi::{ + threadsafe_function::{ + ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, + }, + Env, JsFunction, Status, +}; use napi_derive::napi; -use crate::{ - cast::TryCast, - sync::{await_promise, handle_error}, - threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode}, -}; +use crate::cast::TryCast; #[napi(object)] pub struct ContractAndFunctionName { @@ -38,16 +36,10 @@ impl TryCast<(String, Option)> for ContractAndFunctionName { } } -struct DecodeConsoleLogInputsCall { - inputs: Vec, - sender: Sender>>, -} - struct ContractAndFunctionNameCall { code: Bytes, /// Only present for calls. calldata: Option, - sender: Sender)>>, } #[napi(object)] @@ -247,116 +239,71 @@ pub struct CollapsedMethod { #[derive(Clone)] struct LogCollector { - decode_console_log_inputs_fn: ThreadsafeFunction, - get_contract_and_function_name_fn: ThreadsafeFunction, + decode_console_log_inputs_fn: ThreadsafeFunction, ErrorStrategy::Fatal>, + get_contract_and_function_name_fn: + ThreadsafeFunction, indentation: usize, is_enabled: bool, logs: Vec, - print_line_fn: ThreadsafeFunction<(String, bool)>, + print_line_fn: ThreadsafeFunction<(String, bool), ErrorStrategy::Fatal>, state: LoggingState, title_length: usize, } impl LogCollector { pub fn new(env: &Env, config: LoggerConfig) -> napi::Result { - let decode_console_log_inputs_fn = ThreadsafeFunction::create( - env.raw(), - // SAFETY: The callback is guaranteed to be valid for the lifetime of the tracer. - unsafe { config.decode_console_log_inputs_callback.raw() }, - 0, - |ctx: ThreadSafeCallContext| { - // Bytes[] - let inputs = ctx - .env - .create_array_with_length(ctx.value.inputs.len()) - .and_then(|mut inputs| { - for (idx, input) in ctx.value.inputs.into_iter().enumerate() { - // SAFETY: The input is guaranteed to be valid for the lifetime of the - // JS buffer. - unsafe { - ctx.env.create_buffer_with_borrowed_data( - input.as_ptr(), - input.len(), - input, - |input: Bytes, _env| { - std::mem::drop(input); - }, - ) + let mut decode_console_log_inputs_fn = config + .decode_console_log_inputs_callback + .create_threadsafe_function(0, |ctx: ThreadSafeCallContext>| { + let inputs = + ctx.env + .create_array_with_length(ctx.value.len()) + .and_then(|mut inputs| { + for (idx, input) in ctx.value.into_iter().enumerate() { + ctx.env.create_buffer_with_data(input.to_vec()).and_then( + |input| inputs.set_element(idx as u32, input.into_raw()), + )?; } - .and_then(|input| inputs.set_element(idx as u32, input.into_raw()))?; - } - - Ok(inputs) - })?; - - let sender = ctx.value.sender.clone(); - let promise = ctx.callback.call(None, &[inputs])?; - let result = - await_promise::, Vec>(ctx.env, promise, ctx.value.sender); - - handle_error(sender, result) - }, - )?; - - let get_contract_and_function_name_fn = ThreadsafeFunction::create( - env.raw(), - // SAFETY: The callback is guaranteed to be valid for the lifetime of the tracer. - unsafe { config.get_contract_and_function_name_callback.raw() }, - 0, - |ctx: ThreadSafeCallContext| { - // Buffer - let code = ctx.value.code; - // SAFETY: The code is guaranteed to be valid for the lifetime of the - // JS buffer. - let code = unsafe { - ctx.env.create_buffer_with_borrowed_data( - code.as_ptr(), - code.len(), - code, - |code: Bytes, _env| { - std::mem::drop(code); - }, - ) - }? - .into_unknown(); - - // Option - let calldata = if let Some(calldata) = ctx.value.calldata { - // SAFETY: The calldata is guaranteed to be valid for the lifetime of the - // JS buffer. - unsafe { - ctx.env.create_buffer_with_borrowed_data( - calldata.as_ptr(), - calldata.len(), - calldata, - |calldata: Bytes, _env| { - std::mem::drop(calldata); - }, - ) - }? - .into_unknown() - } else { - ctx.env.get_undefined()?.into_unknown() - }; + Ok(inputs) + })?; - let sender = ctx.value.sender.clone(); + Ok(vec![inputs]) + })?; + + // Maintain a weak reference to the function to avoid the event loop from + // exiting. + decode_console_log_inputs_fn.unref(env)?; + + let mut get_contract_and_function_name_fn = config + .get_contract_and_function_name_callback + .create_threadsafe_function( + 0, + |ctx: ThreadSafeCallContext| { + // Buffer + let code = ctx + .env + .create_buffer_with_data(ctx.value.code.to_vec())? + .into_unknown(); + + // Option + let calldata = if let Some(calldata) = ctx.value.calldata { + ctx.env + .create_buffer_with_data(calldata.to_vec())? + .into_unknown() + } else { + ctx.env.get_undefined()?.into_unknown() + }; - let promise = ctx.callback.call(None, &[code, calldata])?; - let result = await_promise::)>( - ctx.env, - promise, - ctx.value.sender, - ); + Ok(vec![code, calldata]) + }, + )?; - handle_error(sender, result) - }, - )?; + // Maintain a weak reference to the function to avoid the event loop from + // exiting. + get_contract_and_function_name_fn.unref(env)?; - let print_line_fn = ThreadsafeFunction::create( - env.raw(), - // SAFETY: The callback is guaranteed to be valid for the lifetime of the tracer. - unsafe { config.print_line_callback.raw() }, + let mut print_line_fn = config.print_line_callback.create_threadsafe_function( 0, |ctx: ThreadSafeCallContext<(String, bool)>| { // String @@ -365,12 +312,14 @@ impl LogCollector { // bool let replace = ctx.env.get_boolean(ctx.value.1)?; - ctx.callback - .call(None, &[message.into_unknown(), replace.into_unknown()])?; - Ok(()) + Ok(vec![message.into_unknown(), replace.into_unknown()]) }, )?; + // Maintain a weak reference to the function to avoid the event loop from + // exiting. + print_line_fn.unref(env)?; + Ok(Self { decode_console_log_inputs_fn, get_contract_and_function_name_fn, @@ -599,14 +548,21 @@ impl LogCollector { ) -> (String, Option) { let (sender, receiver) = channel(); - let status = self.get_contract_and_function_name_fn.call( - ContractAndFunctionNameCall { - code, - calldata, - sender, - }, - ThreadsafeFunctionCallMode::Blocking, - ); + let status = self + .get_contract_and_function_name_fn + .call_with_return_value( + ContractAndFunctionNameCall { code, calldata }, + ThreadsafeFunctionCallMode::Blocking, + move |result: ContractAndFunctionName| { + let contract_and_function_name = result.try_cast(); + sender.send(contract_and_function_name).map_err(|_error| { + napi::Error::new( + Status::GenericFailure, + "Failed to send result from get_contract_and_function_name", + ) + }) + }, + ); assert_eq!(status, Status::Ok); receiver @@ -793,19 +749,21 @@ impl LogCollector { fn log_console_log_messages(&mut self, console_log_inputs: &[Bytes]) { let (sender, receiver) = channel(); - let status = self.decode_console_log_inputs_fn.call( - DecodeConsoleLogInputsCall { - inputs: console_log_inputs.to_vec(), - sender, - }, + let status = self.decode_console_log_inputs_fn.call_with_return_value( + console_log_inputs.to_vec(), ThreadsafeFunctionCallMode::Blocking, + move |decoded_inputs: Vec| { + sender.send(decoded_inputs).map_err(|_error| { + napi::Error::new( + Status::GenericFailure, + "Failed to send result from decode_console_log_inputs", + ) + }) + }, ); assert_eq!(status, Status::Ok); - let console_log_inputs = receiver - .recv() - .unwrap() - .expect("Failed call to decode_console_log_inputs"); + let console_log_inputs = receiver.recv().unwrap(); // This is a special case, as we always want to print the console.log messages. // The difference is how. If we have a logger, we should use that, so that logs // are printed in order. If we don't, we just print the messages here. diff --git a/crates/edr_napi/src/result.rs b/crates/edr_napi/src/result.rs index 0368d71e2d..4811d51454 100644 --- a/crates/edr_napi/src/result.rs +++ b/crates/edr_napi/src/result.rs @@ -1,5 +1,3 @@ -use std::mem; - use napi::{ bindgen_prelude::{BigInt, Buffer, Either3}, Either, Env, JsBuffer, JsBufferValue, @@ -194,36 +192,19 @@ impl ExecutionResult { logs, output: match output { edr_evm::Output::Call(return_value) => { - let return_value = return_value.clone(); - Either::A(CallOutput { - return_value: unsafe { - env.create_buffer_with_borrowed_data( - return_value.as_ptr(), - return_value.len(), - return_value, - |return_value: edr_eth::Bytes, _env| { - mem::drop(return_value); - }, - ) - } - .map(JsBufferValue::into_raw)?, - }) + let return_value = env + .create_buffer_with_data(return_value.to_vec()) + .map(JsBufferValue::into_raw)?; + + Either::A(CallOutput { return_value }) } edr_evm::Output::Create(return_value, address) => { - let return_value = return_value.clone(); + let return_value = env + .create_buffer_with_data(return_value.to_vec()) + .map(JsBufferValue::into_raw)?; Either::B(CreateOutput { - return_value: unsafe { - env.create_buffer_with_borrowed_data( - return_value.as_ptr(), - return_value.len(), - return_value, - |return_value: edr_eth::Bytes, _env| { - mem::drop(return_value); - }, - ) - } - .map(JsBufferValue::into_raw)?, + return_value, address: address.map(|address| Buffer::from(address.as_slice())), }) } @@ -231,20 +212,13 @@ impl ExecutionResult { }) } edr_evm::ExecutionResult::Revert { gas_used, output } => { - let output = output.clone(); + let output = env + .create_buffer_with_data(output.to_vec()) + .map(JsBufferValue::into_raw)?; + Either3::B(RevertResult { gas_used: BigInt::from(*gas_used), - output: unsafe { - env.create_buffer_with_borrowed_data( - output.as_ptr(), - output.len(), - output, - |output: edr_eth::Bytes, _env| { - mem::drop(output); - }, - ) - } - .map(JsBufferValue::into_raw)?, + output, }) } edr_evm::ExecutionResult::Halt { reason, gas_used } => Either3::C(HaltResult { diff --git a/crates/edr_napi/src/subscribe.rs b/crates/edr_napi/src/subscribe.rs index 7dcd022d02..15963274fe 100644 --- a/crates/edr_napi/src/subscribe.rs +++ b/crates/edr_napi/src/subscribe.rs @@ -1,22 +1,21 @@ use edr_eth::{remote::eth, B256}; -use napi::{bindgen_prelude::BigInt, Env, JsFunction, NapiRaw}; -use napi_derive::napi; - -use crate::threadsafe_function::{ - ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, +use napi::{ + bindgen_prelude::BigInt, + threadsafe_function::{ + ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, + }, + Env, JsFunction, }; +use napi_derive::napi; #[derive(Clone)] pub struct SubscriberCallback { - inner: ThreadsafeFunction, + inner: ThreadsafeFunction, } impl SubscriberCallback { pub fn new(env: &Env, subscription_event_callback: JsFunction) -> napi::Result { - let callback = ThreadsafeFunction::create( - env.raw(), - // SAFETY: The callback is guaranteed to be valid for the lifetime of the inspector. - unsafe { subscription_event_callback.raw() }, + let mut callback = subscription_event_callback.create_threadsafe_function( 0, |ctx: ThreadSafeCallContext| { // SubscriptionEvent @@ -39,10 +38,14 @@ impl SubscriberCallback { event.set_named_property("result", result)?; - ctx.callback.call(None, &[event])?; - Ok(()) + Ok(vec![event]) }, )?; + + // Maintain a weak reference to the function to avoid the event loop from + // exiting. + callback.unref(env)?; + Ok(Self { inner: callback }) } diff --git a/crates/edr_napi/src/sync.rs b/crates/edr_napi/src/sync.rs deleted file mode 100644 index 3cf2cc60c6..0000000000 --- a/crates/edr_napi/src/sync.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::{fmt::Debug, sync::mpsc::Sender}; - -use napi::{bindgen_prelude::FromNapiValue, Env, JsFunction, JsObject, JsUnknown, NapiRaw, Status}; - -use crate::cast::TryCast; - -pub fn await_promise( - env: Env, - result: JsUnknown, - tx: Sender>, -) -> napi::Result<()> -where - I: FromNapiValue + TryCast, - O: 'static, -{ - // If the result is a promise, wait for it to resolve, and send the result to - // the channel. Otherwise, send the result immediately. - if result.is_promise()? { - let result: JsObject = result.try_into()?; - let then: JsFunction = result.get_named_property("then")?; - let tx2 = tx.clone(); - let cb = env.create_function_from_closure("callback", move |ctx| { - let result = ctx.get::(0)?; - tx.send(Ok(result.try_cast()?)).unwrap(); - ctx.env.get_undefined() - })?; - let eb = env.create_function_from_closure("error_callback", move |ctx| { - // TODO: need a way to convert a JsUnknown to an Error - tx2.send(Err(napi::Error::from_reason("Promise rejected"))) - .unwrap(); - ctx.env.get_undefined() - })?; - then.call(Some(&result), &[cb, eb])?; - } else { - let result = unsafe { I::from_napi_value(env.raw(), result.raw())? }; - tx.send(Ok(result.try_cast()?)).unwrap(); - } - - Ok(()) -} - -#[allow(dead_code)] -pub fn await_void_promise( - env: Env, - result: JsUnknown, - tx: Sender>, -) -> napi::Result<()> { - // If the result is a promise, wait for it to resolve, and send the result to - // the channel. Otherwise, send the result immediately. - if result.is_promise()? { - let result: JsObject = result.try_into()?; - let then: JsFunction = result.get_named_property("then")?; - let tx2 = tx.clone(); - let cb = env.create_function_from_closure("callback", move |ctx| { - tx.send(Ok(())).unwrap(); - ctx.env.get_undefined() - })?; - let eb = env.create_function_from_closure("error_callback", move |ctx| { - // TODO: need a way to convert a JsUnknown to an Error - tx2.send(Err(napi::Error::from_reason("Promise rejected"))) - .unwrap(); - ctx.env.get_undefined() - })?; - then.call(Some(&result), &[cb, eb])?; - Ok(()) - } else { - Err(napi::Error::new( - Status::ObjectExpected, - "Expected promise".to_owned(), - )) - } -} - -pub fn handle_error( - tx: Sender>, - res: napi::Result<()>, -) -> napi::Result<()> { - match res { - Ok(_) => Ok(()), - Err(e) => { - tx.send(Err(e)).expect("send error"); - Ok(()) - } - } -} diff --git a/crates/edr_napi/src/threadsafe_function.rs b/crates/edr_napi/src/threadsafe_function.rs deleted file mode 100644 index 1d03c840c4..0000000000 --- a/crates/edr_napi/src/threadsafe_function.rs +++ /dev/null @@ -1,305 +0,0 @@ -// Fork of threadsafe_function from napi-rs that allows calling JS function -// manually rather than only returning args. This enables us to use the return -// value of the function. - -#![allow(clippy::single_component_path_imports)] - -use std::{ - convert::Into, - ffi::CString, - marker::PhantomData, - os::raw::c_void, - ptr, - sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, - }, -}; - -use napi::{check_status, sys, Env, JsError, JsFunction, NapiValue, Result, Status}; - -/// `ThreadSafeFunction` context object -/// the `value` is the value passed to `call` method -pub struct ThreadSafeCallContext { - pub env: Env, - pub value: T, - pub callback: JsFunction, -} - -#[repr(u8)] -pub enum ThreadsafeFunctionCallMode { - NonBlocking, - Blocking, -} - -impl From for sys::napi_threadsafe_function_call_mode { - fn from(value: ThreadsafeFunctionCallMode) -> Self { - match value { - ThreadsafeFunctionCallMode::Blocking => sys::ThreadsafeFunctionCallMode::blocking, - ThreadsafeFunctionCallMode::NonBlocking => sys::ThreadsafeFunctionCallMode::nonblocking, - } - } -} - -/// Communicate with the addon's main thread by invoking a JavaScript function -/// from other threads. -/// -/// ## Example -/// An example of using `ThreadsafeFunction`: -/// -/// ```rust -/// #[macro_use] -/// extern crate napi_derive; -/// -/// use std::thread; -/// -/// use napi::{ -/// threadsafe_function::{ -/// ThreadSafeCallContext, ThreadsafeFunctionCallMode, ThreadsafeFunctionReleaseMode, -/// }, -/// CallContext, Error, JsFunction, JsNumber, JsUndefined, Result, Status, -/// }; -/// -/// #[js_function(1)] -/// pub fn test_threadsafe_function(ctx: CallContext) -> Result { -/// let func = ctx.get::(0)?; -/// -/// let tsfn = -/// ctx -/// .env -/// .create_threadsafe_function(&func, 0, |ctx: ThreadSafeCallContext>| { -/// ctx.value -/// .iter() -/// .map(|v| ctx.env.create_uint32(*v)) -/// .collect::>>() -/// })?; -/// -/// let tsfn_cloned = tsfn.clone(); -/// -/// thread::spawn(move || { -/// let output: Vec = vec![0, 1, 2, 3]; -/// // It's okay to call a threadsafe function multiple times. -/// tsfn.call(Ok(output.clone()), ThreadsafeFunctionCallMode::Blocking); -/// }); -/// -/// thread::spawn(move || { -/// let output: Vec = vec![3, 2, 1, 0]; -/// // It's okay to call a threadsafe function multiple times. -/// tsfn_cloned.call(Ok(output.clone()), ThreadsafeFunctionCallMode::NonBlocking); -/// }); -/// -/// ctx.env.get_undefined() -/// } -/// ``` -#[derive(Debug)] -pub struct ThreadsafeFunction { - raw_tsfn: sys::napi_threadsafe_function, - aborted: Arc, - ref_count: Arc, - _phantom: PhantomData, -} - -impl Clone for ThreadsafeFunction { - fn clone(&self) -> Self { - if !self.aborted.load(Ordering::Acquire) { - let acquire_status = unsafe { sys::napi_acquire_threadsafe_function(self.raw_tsfn) }; - debug_assert!( - acquire_status == sys::Status::napi_ok, - "Acquire threadsafe function failed in clone" - ); - } - - Self { - raw_tsfn: self.raw_tsfn, - aborted: Arc::clone(&self.aborted), - ref_count: Arc::clone(&self.ref_count), - _phantom: PhantomData, - } - } -} - -unsafe impl Send for ThreadsafeFunction {} -unsafe impl Sync for ThreadsafeFunction {} - -impl ThreadsafeFunction { - /// See [napi_create_threadsafe_function](https://nodejs.org/api/n-api.html#n_api_napi_create_threadsafe_function) - /// for more information. - pub(crate) fn create) -> Result<()>>( - env: sys::napi_env, - func: sys::napi_value, - max_queue_size: usize, - callback: R, - ) -> Result { - let mut async_resource_name = ptr::null_mut(); - let s = "napi_rs_threadsafe_function"; - let len = s.len(); - let s = CString::new(s)?; - check_status!(unsafe { - sys::napi_create_string_utf8(env, s.as_ptr(), len, &mut async_resource_name) - })?; - - let initial_thread_count = 1usize; - let mut raw_tsfn = ptr::null_mut(); - let ptr = Box::into_raw(Box::new(callback)).cast::(); - check_status!(unsafe { - sys::napi_create_threadsafe_function( - env, - func, - ptr::null_mut(), - async_resource_name, - max_queue_size, - initial_thread_count, - ptr, - Some(thread_finalize_cb::), - ptr, - Some(call_js_cb::), - &mut raw_tsfn, - ) - })?; - - let aborted = Arc::new(AtomicBool::new(false)); - let aborted_ptr = Arc::into_raw(aborted.clone()) as *mut c_void; - check_status!(unsafe { - sys::napi_add_env_cleanup_hook(env, Some(cleanup_cb), aborted_ptr) - })?; - - Ok(ThreadsafeFunction { - raw_tsfn, - aborted, - ref_count: Arc::new(AtomicUsize::new(initial_thread_count)), - _phantom: PhantomData, - }) - } -} - -impl ThreadsafeFunction { - /// See [napi_call_threadsafe_function](https://nodejs.org/api/n-api.html#n_api_napi_call_threadsafe_function) - /// for more information. - pub fn call(&self, value: T, mode: ThreadsafeFunctionCallMode) -> Status { - if self.aborted.load(Ordering::Acquire) { - return Status::Closing; - } - unsafe { - sys::napi_call_threadsafe_function( - self.raw_tsfn, - Box::into_raw(Box::new(value)).cast(), - mode.into(), - ) - } - .into() - } -} - -impl Drop for ThreadsafeFunction { - fn drop(&mut self) { - if !self.aborted.load(Ordering::Acquire) && self.ref_count.load(Ordering::Acquire) > 0usize - { - let release_status = unsafe { - sys::napi_release_threadsafe_function( - self.raw_tsfn, - sys::ThreadsafeFunctionReleaseMode::release, - ) - }; - assert!( - release_status == sys::Status::napi_ok, - "Threadsafe Function release failed" - ); - } - } -} - -unsafe extern "C" fn cleanup_cb(cleanup_data: *mut c_void) { - let aborted = Arc::::from_raw(cleanup_data.cast()); - aborted.store(true, Ordering::SeqCst); -} - -unsafe extern "C" fn thread_finalize_cb( - _raw_env: sys::napi_env, - finalize_data: *mut c_void, - _finalize_hint: *mut c_void, -) where - R: 'static + Send + FnMut(ThreadSafeCallContext) -> Result<()>, -{ - // cleanup - drop(Box::::from_raw(finalize_data.cast())); -} - -unsafe extern "C" fn call_js_cb( - raw_env: sys::napi_env, - js_callback: sys::napi_value, - context: *mut c_void, - data: *mut c_void, -) where - R: 'static + Send + FnMut(ThreadSafeCallContext) -> Result<()>, -{ - // env and/or callback can be null when shutting down - if raw_env.is_null() || js_callback.is_null() { - return; - } - - let ctx: &mut R = &mut *context.cast::(); - let val: Result = Ok(*Box::::from_raw(data.cast())); - - let mut recv = ptr::null_mut(); - sys::napi_get_undefined(raw_env, &mut recv); - - let ret = val.and_then(|v| { - (ctx)(ThreadSafeCallContext { - env: Env::from_raw(raw_env), - value: v, - callback: JsFunction::from_raw(raw_env, js_callback).unwrap(), // TODO: unwrap - }) - }); - - let status = match ret { - Ok(()) => sys::Status::napi_ok, - Err(e) => sys::napi_fatal_exception(raw_env, JsError::from(e).into_value(raw_env)), - }; - if status == sys::Status::napi_ok { - return; - } - if status == sys::Status::napi_pending_exception { - let mut error_result = ptr::null_mut(); - assert_eq!( - sys::napi_get_and_clear_last_exception(raw_env, &mut error_result), - sys::Status::napi_ok - ); - - // When shutting down, napi_fatal_exception sometimes returns another exception - let stat = sys::napi_fatal_exception(raw_env, error_result); - assert!(stat == sys::Status::napi_ok || stat == sys::Status::napi_pending_exception); - } else { - let error_code: Status = status.into(); - let error_code_string = format!("{error_code:?}"); - let mut error_code_value = ptr::null_mut(); - assert_eq!( - sys::napi_create_string_utf8( - raw_env, - error_code_string.as_ptr().cast(), - error_code_string.len(), - &mut error_code_value, - ), - sys::Status::napi_ok, - ); - let error_msg = "Call JavaScript callback failed in thread safe function"; - let mut error_msg_value = ptr::null_mut(); - assert_eq!( - sys::napi_create_string_utf8( - raw_env, - error_msg.as_ptr().cast(), - error_msg.len(), - &mut error_msg_value, - ), - sys::Status::napi_ok, - ); - let mut error_value = ptr::null_mut(); - assert_eq!( - sys::napi_create_error(raw_env, error_code_value, error_msg_value, &mut error_value), - sys::Status::napi_ok, - ); - assert_eq!( - sys::napi_fatal_exception(raw_env, error_value), - sys::Status::napi_ok - ); - } -} diff --git a/crates/edr_napi/src/trace.rs b/crates/edr_napi/src/trace.rs index e27f353fae..4ec3ee7572 100644 --- a/crates/edr_napi/src/trace.rs +++ b/crates/edr_napi/src/trace.rs @@ -1,4 +1,4 @@ -use std::{mem, sync::Arc}; +use std::sync::Arc; use edr_evm::{interpreter::OPCODE_JUMPMAP, trace::BeforeMessage}; use napi::{ @@ -47,34 +47,14 @@ pub struct TracingMessage { impl TracingMessage { pub fn new(env: &Env, message: &BeforeMessage) -> napi::Result { - let data = message.data.clone(); - let data = unsafe { - env.create_buffer_with_borrowed_data( - data.as_ptr(), - data.len(), - data, - |data: edr_eth::Bytes, _env| { - mem::drop(data); - }, - ) - } - .map(JsBufferValue::into_raw)?; + let data = env + .create_buffer_with_data(message.data.to_vec()) + .map(JsBufferValue::into_raw)?; let code = message.code.as_ref().map_or(Ok(None), |code| { - let code = code.original_bytes(); - - unsafe { - env.create_buffer_with_borrowed_data( - code.as_ptr(), - code.len(), - code, - |code: edr_eth::Bytes, _env| { - mem::drop(code); - }, - ) - } - .map(JsBufferValue::into_raw) - .map(Some) + env.create_buffer_with_data(code.original_bytes().to_vec()) + .map(JsBufferValue::into_raw) + .map(Some) })?; Ok(TracingMessage { diff --git a/e2e/fixture-projects/script/hardhat.config.js b/e2e/fixture-projects/script/hardhat.config.js new file mode 100644 index 0000000000..50373a2a35 --- /dev/null +++ b/e2e/fixture-projects/script/hardhat.config.js @@ -0,0 +1,3 @@ +module.exports = { + solidity: "0.8.20" +} diff --git a/e2e/fixture-projects/script/package.json b/e2e/fixture-projects/script/package.json new file mode 100644 index 0000000000..a4f92ca9f0 --- /dev/null +++ b/e2e/fixture-projects/script/package.json @@ -0,0 +1,3 @@ +{ + "name": "script" +} diff --git a/e2e/fixture-projects/script/script.js b/e2e/fixture-projects/script/script.js new file mode 100644 index 0000000000..3bdf655bc4 --- /dev/null +++ b/e2e/fixture-projects/script/script.js @@ -0,0 +1,9 @@ +const assert = require("assert") + +async function main() { + const blockNumber = await hre.network.provider.send("eth_blockNumber") + assert.equal(blockNumber, "0x0") +} + +main() + .catch(console.error) diff --git a/e2e/fixture-projects/script/test.sh b/e2e/fixture-projects/script/test.sh new file mode 100755 index 0000000000..ccfd232e4a --- /dev/null +++ b/e2e/fixture-projects/script/test.sh @@ -0,0 +1,12 @@ +#! /usr/bin/env sh + +# fail if any commands fails +set -e + +# import helpers functions +. ../helpers.sh + +echo "Running tests: $(basename "$(pwd)")" + +echo "it should run a script that uses the hardhat network provider" +run_test_and_handle_failure "npx hardhat run script.js" 0 diff --git a/packages/hardhat-core/package.json b/packages/hardhat-core/package.json index 8e9b73eeb0..9bdf10c608 100644 --- a/packages/hardhat-core/package.json +++ b/packages/hardhat-core/package.json @@ -28,11 +28,11 @@ "eslint": "eslint 'src/**/*.ts' 'test/**/*.ts'", "prettier": "prettier \"**/*.{js,md,json}\"", "pretest": "cd ../.. && pnpm build", - "test": "mocha --recursive \"test/**/*.ts\" --exit", - "test:except-provider": "mocha --recursive \"test/**/*.ts\" --exclude \"test/internal/hardhat-network/provider/**/*.ts\" --exit", - "test:except-tracing": "mocha --recursive \"test/**/*.ts\" --exclude \"test/internal/hardhat-network/stack-traces/**/*.ts\" --exit", - "test:provider": "mocha --recursive \"test/internal/hardhat-network/provider/**/*.ts\" --exit", - "test:tracing": "mocha --recursive \"test/internal/hardhat-network/stack-traces/**/*.ts\" --exit", + "test": "mocha --recursive \"test/**/*.ts\"", + "test:except-provider": "mocha --recursive \"test/**/*.ts\" --exclude \"test/internal/hardhat-network/provider/**/*.ts\"", + "test:except-tracing": "mocha --recursive \"test/**/*.ts\" --exclude \"test/internal/hardhat-network/stack-traces/**/*.ts\"", + "test:provider": "mocha --recursive \"test/internal/hardhat-network/provider/**/*.ts\"", + "test:tracing": "mocha --recursive \"test/internal/hardhat-network/stack-traces/**/*.ts\"", "pretest:provider": "cd ../../crates/edr_napi && pnpm build", "pretest:except-provider": "cd ../../crates/edr_napi && pnpm build", "pretest:tracing": "cd ../../crates/edr_napi && pnpm build", @@ -62,9 +62,9 @@ "console.sol" ], "devDependencies": { - "@nomicfoundation/ethereumjs-block": "5.0.4", "@nomicfoundation/eslint-plugin-hardhat-internal-rules": "workspace:^", "@nomicfoundation/eslint-plugin-slow-imports": "workspace:^", + "@nomicfoundation/ethereumjs-block": "5.0.4", "@types/async-eventemitter": "^0.2.1", "@types/bn.js": "^5.1.0", "@types/chai": "^4.2.0",