diff --git a/mm2src/mm2_bin_lib/src/lib.rs b/mm2src/mm2_bin_lib/src/lib.rs index 713121b373..453f1b55f5 100644 --- a/mm2src/mm2_bin_lib/src/lib.rs +++ b/mm2src/mm2_bin_lib/src/lib.rs @@ -108,3 +108,20 @@ async fn finalize_mm2_stop(ctx: MmArc) { dispatch_lp_event(ctx.clone(), StopCtxEvent.into()).await; let _ = ctx.stop().await; } + +#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize))] +#[derive(Clone, Copy, Debug, PartialEq, Primitive)] +pub enum StartupResultCode { + /// Operation completed successfully + Ok = 0, + /// Invalid parameters were provided to the function + InvalidParams = 1, + /// The configuration was invalid (missing required fields, etc.) + ConfigError = 2, + /// MM2 is already running + AlreadyRunning = 3, + /// MM2 initialization failed + InitError = 4, + /// Failed to spawn the MM2 process/thread + SpawnError = 5, +} diff --git a/mm2src/mm2_bin_lib/src/mm2_native_lib.rs b/mm2src/mm2_bin_lib/src/mm2_native_lib.rs index f08a0f756b..8ca51fd4f2 100644 --- a/mm2src/mm2_bin_lib/src/mm2_native_lib.rs +++ b/mm2src/mm2_bin_lib/src/mm2_native_lib.rs @@ -7,6 +7,7 @@ use enum_primitive_derive::Primitive; use gstuff::any_to_str; use libc::c_char; use mm2_core::mm_ctx::MmArc; +use mm2_main::LpMainParams; use num_traits::FromPrimitive; use serde_json::{self as json}; use std::ffi::{CStr, CString}; @@ -15,15 +16,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; use std::time::Duration; -#[derive(Debug, PartialEq, Primitive)] -enum MainErr { - Ok = 0, - AlreadyRuns = 1, - ConfIsNull = 2, - ConfNotUtf8 = 3, - CantThread = 5, -} - /// Starts the MM2 in a detached singleton thread. #[no_mangle] #[allow(clippy::missing_safety_doc)] @@ -48,40 +40,101 @@ pub unsafe extern "C" fn mm2_main(conf: *const c_char, log_cb: extern "C" fn(lin }}; } - if LP_MAIN_RUNNING.load(Ordering::Relaxed) { - eret!(MainErr::AlreadyRuns) - } - CTX.store(0, Ordering::Relaxed); // Remove the old context ID during restarts. - if conf.is_null() { - eret!(MainErr::ConfIsNull) + eret!(StartupResultCode::InvalidParams, "Configuration is null") } - let conf = CStr::from_ptr(conf); - let conf = match conf.to_str() { + let conf_cstr = CStr::from_ptr(conf); + let conf_str = match conf_cstr.to_str() { Ok(s) => s, - Err(e) => eret!(MainErr::ConfNotUtf8, e), + Err(e) => eret!( + StartupResultCode::InvalidParams, + format!("Configuration is not valid UTF-8: {}", e) + ), + }; + + let conf: json::Value = match json::from_str(conf_str) { + Ok(v) => v, + Err(e) => eret!( + StartupResultCode::ConfigError, + format!("Failed to parse configuration: {}", e) + ), }; - let conf = conf.to_owned(); + + if let Err(true) = LP_MAIN_RUNNING.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) { + eret!(StartupResultCode::AlreadyRunning, "MM2 is already running"); + } + + // Remove the old context ID during restarts. + CTX.store(0, Ordering::Relaxed); register_callback(FfiCallback::with_ffi_function(log_cb)); - let rc = thread::Builder::new().name("lp_main".into()).spawn(move || { - if let Err(true) = LP_MAIN_RUNNING.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) { - log!("lp_main already started!"); - return; - } - let ctx_cb = &|ctx| CTX.store(ctx, Ordering::Relaxed); - match catch_unwind(move || mm2_main::run_lp_main(Some(&conf), ctx_cb, KDF_VERSION.into(), KDF_DATETIME.into())) - { - Ok(Ok(_)) => log!("run_lp_main finished"), - Ok(Err(err)) => log!("run_lp_main error: {}", err), - Err(err) => log!("run_lp_main panic: {:?}", any_to_str(&*err)), + + let ctx_cb = &|ctx| CTX.store(ctx, Ordering::Relaxed); + let params = LpMainParams::with_conf(conf).log_filter(None); + + let ctx = match catch_unwind(|| { + block_on(mm2_main::lp_main( + params, + &ctx_cb, + KDF_VERSION.into(), + KDF_DATETIME.into(), + )) + }) { + Ok(Ok(ctx)) => ctx, + Ok(Err(e)) => { + log!("MM2 initialization failed: {}", e); + LP_MAIN_RUNNING.store(false, Ordering::Relaxed); + return StartupResultCode::InitError as i8; + }, + Err(err) => { + log!("MM2 initialization panicked: {:?}", any_to_str(&*err)); + LP_MAIN_RUNNING.store(false, Ordering::Relaxed); + return StartupResultCode::InitError as i8; + }, + }; + + // This allows us to use catch_unwind inside the lp_run thread despite MmCtx containing + // types with interior mutability (Mutex, RwLock, etc.) that aren't UnwindSafe. + // By passing just a numeric ID and recovering the actual context inside the + // catch_unwind block, we satisfy the compiler's safety requirements while properly + // handling potential panics. + let ctx_id = match ctx.ffi_handle() { + Ok(id) => id, + Err(e) => { + log!("MM2 thread setup failed: Failed to create FFI handle: {}", e); + LP_MAIN_RUNNING.store(false, Ordering::Relaxed); + return StartupResultCode::InitError as i8; + }, + }; + + let rc = thread::Builder::new().name("lp_run".into()).spawn(move || { + match catch_unwind(move || { + let ctx = match MmArc::from_ffi_handle(ctx_id) { + Ok(ctx) => ctx, + Err(err) => { + panic!("Failed to recover context in thread: {}", err); + }, + }; + block_on(mm2_main::lp_run(ctx)); + }) { + Ok(_) => log!("MM2 thread completed normally"), + Err(err) => { + log!("MM2 thread panicked: {:?}", any_to_str(&*err)); + }, }; + LP_MAIN_RUNNING.store(false, Ordering::Relaxed) }); + if let Err(e) = rc { - eret!(MainErr::CantThread, e) + LP_MAIN_RUNNING.store(false, Ordering::Relaxed); + eret!( + StartupResultCode::SpawnError, + format!("Failed to spawn MM2 thread: {:?}", e) + ); } - MainErr::Ok as i8 + + StartupResultCode::Ok as i8 } /// Checks if the MM2 singleton thread is currently running or not. @@ -109,7 +162,7 @@ pub extern "C" fn mm2_test(torch: i32, log_cb: extern "C" fn(line: *const c_char static RUNNING: AtomicBool = AtomicBool::new(false); if let Err(true) = RUNNING.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) { log!("mm2_test] Running already!"); - return -1; + return StartupResultCode::AlreadyRunning as i32; } // #402: Stop the MM in order to test the library restart. @@ -120,7 +173,7 @@ pub extern "C" fn mm2_test(torch: i32, log_cb: extern "C" fn(line: *const c_char Ok(ctx) => ctx, Err(err) => { log!("mm2_test] Invalid CTX? !from_ffi_handle: {}", err); - return -1; + return StartupResultCode::InvalidParams as i32; }, }; let conf = json::to_string(&ctx.conf).unwrap(); @@ -177,10 +230,10 @@ pub extern "C" fn mm2_test(torch: i32, log_cb: extern "C" fn(line: *const c_char log!("mm2_test] Restarting MM…"); let conf = CString::new(&conf[..]).unwrap(); let rc = unsafe { mm2_main(conf.as_ptr(), log_cb) }; - let rc = MainErr::from_i8(rc).unwrap(); - if rc != MainErr::Ok { + let rc = StartupResultCode::from_i8(rc).unwrap(); + if rc != StartupResultCode::Ok { log!("!mm2_main: {:?}", rc); - return -1; + return rc as i32; } // Wait for the new MM instance to allocate context. @@ -192,14 +245,14 @@ pub extern "C" fn mm2_test(torch: i32, log_cb: extern "C" fn(line: *const c_char } if now_float() - since > 60.0 { log!("mm2_test] Won't start"); - return -1; + return StartupResultCode::InitError as i32; } } let ctx_id = CTX.load(Ordering::Relaxed); if ctx_id == prev_ctx_id { log!("mm2_test] Context ID is the same"); - return -1; + return StartupResultCode::InvalidParams as i32; } log!("mm2_test] New MM instance {} started", ctx_id); } diff --git a/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs b/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs index deaec8fedb..9fcf795012 100644 --- a/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs +++ b/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs @@ -1,8 +1,5 @@ //! Some specifics of using the [`wasm_bindgen`] library: //! -//! # Currently only `Result` is allowed -//! [tracking issue]: https://github.com/rustwasm/wasm-bindgen/issues/1004 -//! //! # JavaScript enums do not support methods at all //! [tracking issue]: https://github.com/rustwasm/wasm-bindgen/issues/1715 //! @@ -21,18 +18,27 @@ use mm2_rpc::data::legacy::MmVersionResponse; use mm2_rpc::wasm_rpc::WasmRpcResponse; use serde::{Deserialize, Serialize}; use serde_json::Value as Json; - -/// The errors can be thrown when using the `mm2_main` function incorrectly. #[wasm_bindgen] -#[derive(Primitive)] -pub enum Mm2MainErr { - AlreadyRuns = 1, - InvalidParams = 2, - NoCoinsInConf = 3, +#[derive(Debug, Clone, Serialize)] +struct StartupError { + code: StartupResultCode, + message: String, } -impl From for JsValue { - fn from(e: Mm2MainErr) -> Self { JsValue::from(e as i32) } +#[wasm_bindgen] +impl StartupError { + fn new(code: StartupResultCode, message: impl Into) -> Self { + Self { + code, + message: message.into(), + } + } + + #[wasm_bindgen(getter)] + pub fn code(&self) -> i8 { self.code as i8 } + + #[wasm_bindgen(getter)] + pub fn message(&self) -> String { self.message.clone() } } #[derive(Deserialize)] @@ -58,7 +64,7 @@ impl From for LpMainParams { /// # Usage /// /// ```javascript -/// import init, {mm2_main, LogLevel, Mm2MainErr} from "./path/to/mm2.js"; +/// import init, {mm2_main, LogLevel, StartupResultCode} from "./path/to/mm2.js"; /// /// const params = { /// conf: { "gui":"WASMTEST", mm2:1, "passphrase":"YOUR_PASSPHRASE_HERE", "rpc_password":"test123", "coins":[{"coin":"ETH","protocol":{"type":"ETH"}}] }, @@ -68,8 +74,8 @@ impl From for LpMainParams { /// try { /// mm2_main(params, handle_log); /// } catch (e) { -/// switch (e) { -/// case Mm2MainErr.AlreadyRuns: +/// switch (e.code) { +/// case StartupResultCode.AlreadyRunning: /// alert("MarketMaker2 already runs..."); /// break; /// // handle other errors... @@ -80,51 +86,70 @@ impl From for LpMainParams { /// } /// ``` #[wasm_bindgen] -pub fn mm2_main(params: JsValue, log_cb: js_sys::Function) -> Result<(), JsValue> { +pub async fn mm2_main(params: JsValue, log_cb: js_sys::Function) -> Result { let params: MainParams = match deserialize_from_js(params.clone()) { Ok(p) => p, Err(e) => { - console_err!("Expected 'MainParams' as the first argument, found {:?}: {}", params, e); - return Err(Mm2MainErr::InvalidParams.into()); + let error = StartupError::new( + StartupResultCode::InvalidParams, + format!("Expected 'MainParams' as the first argument, found {:?}: {}", params, e), + ); + console_err!("{}", error.message()); + return Err(error.into()); }, }; if params.conf["coins"].is_null() { - console_err!("Config must contain 'coins' field: {:?}", params.conf); - return Err(Mm2MainErr::NoCoinsInConf.into()); + let error = StartupError::new( + StartupResultCode::ConfigError, + format!("Config must contain 'coins' field: {:?}", params.conf), + ); + console_err!("{}", error.message()); + return Err(error.into()); } let params = LpMainParams::from(params); if LP_MAIN_RUNNING.load(Ordering::Relaxed) { - return Err(Mm2MainErr::AlreadyRuns.into()); + let error = StartupError::new(StartupResultCode::AlreadyRunning, "MM2 is already running"); + console_err!("{}", error.message()); + return Err(error.into()); } - CTX.store(0, Ordering::Relaxed); // Remove the old context ID during restarts. + + // Remove the old context ID during restarts. + CTX.store(0, Ordering::Relaxed); register_callback(WasmCallback::with_js_function(log_cb)); + // Setting up a global panic hook to log panic information to the console + // This doesn't prevent termination of the WebAssembly instance, but ensures error details are visible + // We can't use catch_unwind directly with async/await in WebAssembly, so this is our best option for diagnostics + // If a panic occurs, the MM2 instance will terminate but the browser tab will remain responsive set_panic_hook(); - let fut = async move { - if let Err(true) = LP_MAIN_RUNNING.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) { - console_err!("lp_main already started!"); - return; - } - let ctx_cb = |ctx| CTX.store(ctx, Ordering::Relaxed); - // TODO figure out how to use catch_unwind here - // use futures::FutureExt; - // match mm2_main::lp_main(params, &ctx_cb).catch_unwind().await { - // Ok(Ok(_)) => console_info!("run_lp_main finished"), - // Ok(Err(err)) => console_err!("run_lp_main error: {}", err), - // Err(err) => console_err!("run_lp_main panic: {:?}", any_to_str(&*err)), - // }; - match mm2_main::lp_main(params, &ctx_cb, KDF_VERSION.into(), KDF_DATETIME.into()).await { - Ok(()) => console_info!("run_lp_main finished"), - Err(err) => console_err!("run_lp_main error: {}", err), - }; - LP_MAIN_RUNNING.store(false, Ordering::Relaxed) + if let Err(true) = LP_MAIN_RUNNING.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) { + let error = StartupError::new(StartupResultCode::AlreadyRunning, "lp_main already started!"); + console_err!("{}", error.message()); + return Err(error.into()); + } + + let ctx_cb = |ctx| CTX.store(ctx, Ordering::Relaxed); + let ctx = match mm2_main::lp_main(params, &ctx_cb, KDF_VERSION.into(), KDF_DATETIME.into()).await { + Ok(ctx) => { + console_info!("lp_main finished"); + ctx + }, + Err(err) => { + let error = StartupError::new(StartupResultCode::InitError, format!("lp_main error: {}", err)); + console_err!("{}", error.message()); + LP_MAIN_RUNNING.store(false, Ordering::Relaxed); + return Err(error.into()); + }, }; - // At this moment we still don't have `MmCtx` context to use its `MmCtx::abortable_system` spawner. - executor::spawn_local(fut); - Ok(()) + executor::spawn_local(async move { + mm2_main::lp_run(ctx).await; + LP_MAIN_RUNNING.store(false, Ordering::Relaxed); + }); + + Ok(StartupResultCode::Ok as i8) } /// Returns the MarketMaker2 instance status. diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index 6a3edf0e10..fc37bc8624 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -25,13 +25,12 @@ use crate::lp_message_service::{init_message_service, InitMessageServiceError}; use crate::lp_network::{lp_network_ports, p2p_event_process_loop, subscribe_to_topic, NetIdError}; use crate::lp_ordermatch::{broadcast_maker_orders_keep_alive_loop, clean_memory_loop, init_ordermatch_context, lp_ordermatch_loop, orders_kick_start, BalanceUpdateOrdermatchHandler, OrdermatchInitError}; -use crate::lp_swap; use crate::lp_swap::swap_kick_starts; use crate::lp_wallet::{initialize_wallet_passphrase, WalletInitError}; use crate::rpc::spawn_rpc; use bitcrypto::sha256; use coins::register_balance_update_handler; -use common::executor::{SpawnFuture, Timer}; +use common::executor::SpawnFuture; use common::log::{info, warn}; use crypto::{from_hw_error, CryptoCtx, HwError, HwProcessingError, HwRpcError, WithHwRpcError}; use derive_more::Display; @@ -516,16 +515,6 @@ pub async fn lp_init(ctx: MmArc, version: String, datetime: String) -> MmInitRes } }); - // In the mobile version we might depend on `lp_init` staying around until the context stops. - loop { - if ctx.is_stopping() { - break; - }; - Timer::sleep(0.2).await - } - // Clearing up the running swaps removes any circular references that might prevent the context from being dropped. - lp_swap::clear_running_swaps(&ctx); - Ok(()) } diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index 65a10e3ea6..9e7e465a78 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -42,10 +42,11 @@ #[cfg(not(target_arch = "wasm32"))] use common::block_on; use common::crash_reports::init_crash_reports; +use common::executor::Timer; use common::log; use common::log::LogLevel; use common::password_policy::password_policy; -use mm2_core::mm_ctx::MmCtxBuilder; +use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; #[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] use common::log::warn; @@ -126,7 +127,7 @@ pub async fn lp_main( ctx_cb: &dyn Fn(u32), version: String, datetime: String, -) -> Result<(), String> { +) -> Result { let log_filter = params.filter.unwrap_or_default(); // Logger can be initialized once. // If `kdf` is linked as a library, and `kdf` is restarted, `init_logger` returns an error. @@ -166,15 +167,28 @@ pub async fn lp_main( #[cfg(not(target_arch = "wasm32"))] spawn_ctrl_c_handler(ctx.clone()); - try_s!(lp_init(ctx, version, datetime).await); - Ok(()) + try_s!(lp_init(ctx.clone(), version, datetime).await); + Ok(ctx) +} + +pub async fn lp_run(ctx: MmArc) { + // In the mobile version we might depend on `lp_init` staying around until the context stops. + loop { + if ctx.is_stopping() { + break; + }; + Timer::sleep(0.2).await + } + + // Clearing up the running swaps removes any circular references that might prevent the context from being dropped. + lp_swap::clear_running_swaps(&ctx); } /// Handles CTRL-C signals and shutdowns the KDF runtime gracefully. /// /// It's important to spawn this task as soon as `Ctx` is in the correct state. #[cfg(not(target_arch = "wasm32"))] -fn spawn_ctrl_c_handler(ctx: mm2_core::mm_ctx::MmArc) { +fn spawn_ctrl_c_handler(ctx: MmArc) { use crate::lp_dispatcher::{dispatch_lp_event, StopCtxEvent}; common::executor::spawn(async move { @@ -369,7 +383,8 @@ pub fn run_lp_main( let log_filter = LogLevel::from_env(); let params = LpMainParams::with_conf(conf).log_filter(log_filter); - try_s!(block_on(lp_main(params, ctx_cb, version, datetime))); + let ctx = try_s!(block_on(lp_main(params, ctx_cb, version, datetime))); + block_on(lp_run(ctx)); Ok(()) } diff --git a/mm2src/mm2_main/src/wasm_tests.rs b/mm2src/mm2_main/src/wasm_tests.rs index 6aace5bfbc..11d31fb91c 100644 --- a/mm2src/mm2_main/src/wasm_tests.rs +++ b/mm2src/mm2_main/src/wasm_tests.rs @@ -1,4 +1,4 @@ -use crate::lp_init; +use crate::{lp_init, lp_run}; use common::executor::{spawn, spawn_abortable, spawn_local_abortable, AbortOnDropHandle, Timer}; use common::log::warn; use common::log::wasm_log::register_wasm_log; @@ -22,7 +22,8 @@ const STOP_TIMEOUT_MS: u64 = 1000; /// Starts the WASM version of MM. fn wasm_start(ctx: MmArc) { spawn(async move { - lp_init(ctx, "TEST".into(), "TEST".into()).await.unwrap(); + lp_init(ctx.clone(), "TEST".into(), "TEST".into()).await.unwrap(); + lp_run(ctx).await; }) } diff --git a/mm2src/mm2_main/tests/integration_tests_common/mod.rs b/mm2src/mm2_main/tests/integration_tests_common/mod.rs index 0c22f3c8bd..170915f681 100644 --- a/mm2src/mm2_main/tests/integration_tests_common/mod.rs +++ b/mm2src/mm2_main/tests/integration_tests_common/mod.rs @@ -2,7 +2,7 @@ use common::executor::Timer; use common::log::LogLevel; use common::{block_on, log, now_ms, wait_until_ms}; use crypto::privkey::key_pair_from_seed; -use mm2_main::{lp_main, LpMainParams}; +use mm2_main::{lp_main, lp_run, LpMainParams}; use mm2_rpc::data::legacy::CoinInitResponse; use mm2_test_helpers::electrums::{doc_electrums, marty_electrums}; use mm2_test_helpers::for_tests::{create_new_account_status, enable_native as enable_native_impl, @@ -26,7 +26,8 @@ pub fn test_mm_start_impl() { log!("test_mm_start] Starting the MarketMaker..."); let conf: Json = json::from_str(&conf).unwrap(); let params = LpMainParams::with_conf(conf).log_filter(Some(filter)); - block_on(lp_main(params, &|_ctx| (), "TEST".into(), "TEST".into())).unwrap() + let ctx = block_on(lp_main(params, &|_ctx| (), "TEST".into(), "TEST".into())).unwrap(); + block_on(lp_run(ctx)) } } }