Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions mm2src/mm2_bin_lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
131 changes: 92 additions & 39 deletions mm2src/mm2_bin_lib/src/mm2_native_lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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)]
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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();
Expand Down Expand Up @@ -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.
Expand All @@ -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);
}
Expand Down
111 changes: 68 additions & 43 deletions mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
//! Some specifics of using the [`wasm_bindgen`] library:
//!
//! # Currently only `Result<T, JsValue>` 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
//!
Expand All @@ -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<Mm2MainErr> for JsValue {
fn from(e: Mm2MainErr) -> Self { JsValue::from(e as i32) }
#[wasm_bindgen]
impl StartupError {
fn new(code: StartupResultCode, message: impl Into<String>) -> 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)]
Expand All @@ -58,7 +64,7 @@ impl From<MainParams> 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"}}] },
Expand All @@ -68,8 +74,8 @@ impl From<MainParams> 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...
Expand All @@ -80,51 +86,70 @@ impl From<MainParams> 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<i8, JsValue> {
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());
},
Comment thread
borngraced marked this conversation as resolved.
};

// 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.
Expand Down
Loading