|
1 | 1 | use std::cmp::Ordering; |
2 | 2 | use std::os::raw::c_char; |
3 | 3 | use std::ffi::{CString, CStr, c_void}; |
4 | | -use std::path::Path; |
| 4 | +use std::path::{Path, PathBuf}; |
5 | 5 | use std::sync::Arc; |
| 6 | +use std::sync::Once; |
6 | 7 | use rand::thread_rng; |
7 | 8 | use serde::{Deserialize, Serialize}; |
8 | 9 | use uuid::Uuid; |
@@ -102,6 +103,61 @@ macro_rules! ensure_wallet ( |
102 | 103 | ) |
103 | 104 | ); |
104 | 105 |
|
| 106 | +static PANIC_HOOK_INIT: Once = Once::new(); |
| 107 | + |
| 108 | +/// Install a panic hook that appends panic info + backtrace to a log file so |
| 109 | +/// silent FFI panics (especially from worker threads detached from stderr) |
| 110 | +/// leave evidence. Idempotent across calls and threads thanks to `Once`. |
| 111 | +fn install_panic_hook() { |
| 112 | + PANIC_HOOK_INIT.call_once(|| { |
| 113 | + // Stack Wallet's Linux data dir is ~/.stackwallet (legacy naming), |
| 114 | + // not the XDG default. Write there so panic logs sit next to wallet |
| 115 | + // data the user can grab. Fall back to /tmp if HOME is unset. |
| 116 | + let log_path = std::env::var_os("HOME") |
| 117 | + .map(PathBuf::from) |
| 118 | + .map(|p| p.join(".stackwallet/flutter_libmwc-panic.log")) |
| 119 | + .unwrap_or_else(|| PathBuf::from("/tmp/flutter_libmwc-panic.log")); |
| 120 | + if let Some(parent) = log_path.parent() { |
| 121 | + let _ = std::fs::create_dir_all(parent); |
| 122 | + } |
| 123 | + let previous = std::panic::take_hook(); |
| 124 | + std::panic::set_hook(Box::new(move |info| { |
| 125 | + let payload = info.payload(); |
| 126 | + let msg = if let Some(s) = payload.downcast_ref::<&str>() { |
| 127 | + (*s).to_string() |
| 128 | + } else if let Some(s) = payload.downcast_ref::<String>() { |
| 129 | + s.clone() |
| 130 | + } else { |
| 131 | + "<non-string panic payload>".to_string() |
| 132 | + }; |
| 133 | + let location = info.location() |
| 134 | + .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column())) |
| 135 | + .unwrap_or_else(|| "<unknown>".to_string()); |
| 136 | + let backtrace = std::backtrace::Backtrace::force_capture(); |
| 137 | + let timestamp = std::time::SystemTime::now() |
| 138 | + .duration_since(std::time::UNIX_EPOCH) |
| 139 | + .map(|d| d.as_secs()) |
| 140 | + .unwrap_or(0); |
| 141 | + let thread = std::thread::current(); |
| 142 | + let thread_name = thread.name().unwrap_or("<unnamed>"); |
| 143 | + let entry = format!( |
| 144 | + "\n=== flutter_libmwc panic at unix={} ===\nthread: {}\nlocation: {}\nmessage: {}\nbacktrace:\n{}\n", |
| 145 | + timestamp, thread_name, location, msg, backtrace, |
| 146 | + ); |
| 147 | + if let Ok(mut f) = std::fs::OpenOptions::new() |
| 148 | + .create(true) |
| 149 | + .append(true) |
| 150 | + .open(&log_path) |
| 151 | + { |
| 152 | + use std::io::Write; |
| 153 | + let _ = f.write_all(entry.as_bytes()); |
| 154 | + let _ = f.flush(); |
| 155 | + } |
| 156 | + previous(info); |
| 157 | + })); |
| 158 | + }); |
| 159 | +} |
| 160 | + |
105 | 161 |
|
106 | 162 | fn init_logger() { |
107 | 163 | android_logger::init_once( |
@@ -276,6 +332,7 @@ pub unsafe extern "C" fn mwc_rust_open_wallet( |
276 | 332 | config: *const c_char, |
277 | 333 | password: *const c_char, |
278 | 334 | ) -> *const c_char { |
| 335 | + install_panic_hook(); |
279 | 336 | //init_logger(); |
280 | 337 | let result = match _open_wallet( |
281 | 338 | config, |
@@ -475,36 +532,55 @@ pub unsafe extern "C" fn mwc_rust_wallet_scan_outputs( |
475 | 532 | start_height: *const c_char, |
476 | 533 | number_of_blocks: *const c_char, |
477 | 534 | ) -> *const c_char { |
478 | | - let wallet_ptr = CStr::from_ptr(wallet); |
479 | | - let c_start_height = CStr::from_ptr(start_height); |
480 | | - let c_number_of_blocks = CStr::from_ptr(number_of_blocks); |
481 | | - let start_height: u64 = c_start_height.to_str().unwrap().to_string().parse().unwrap(); |
482 | | - let number_of_blocks: u64 = c_number_of_blocks.to_str().unwrap().to_string().parse().unwrap(); |
483 | | - |
484 | | - let wallet_data = wallet_ptr.to_str().unwrap(); |
485 | | - let tuple_wallet_data: (u64, Option<SecretKey>) = serde_json::from_str(wallet_data).unwrap(); |
486 | | - let wlt = tuple_wallet_data.0; |
487 | | - let sek_key = tuple_wallet_data.1; |
488 | | - |
489 | | - ensure_wallet!(wlt, wallet); |
| 535 | + install_panic_hook(); |
| 536 | + let wallet_ptr_raw = wallet; |
| 537 | + let start_height_raw = start_height; |
| 538 | + let number_of_blocks_raw = number_of_blocks; |
| 539 | + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { |
| 540 | + let wallet_ptr = CStr::from_ptr(wallet_ptr_raw); |
| 541 | + let c_start_height = CStr::from_ptr(start_height_raw); |
| 542 | + let c_number_of_blocks = CStr::from_ptr(number_of_blocks_raw); |
| 543 | + let start_height: u64 = c_start_height.to_str().unwrap().to_string().parse().unwrap(); |
| 544 | + let number_of_blocks: u64 = c_number_of_blocks.to_str().unwrap().to_string().parse().unwrap(); |
| 545 | + |
| 546 | + let wallet_data = wallet_ptr.to_str().unwrap(); |
| 547 | + let tuple_wallet_data: (u64, Option<SecretKey>) = serde_json::from_str(wallet_data).unwrap(); |
| 548 | + let wlt = tuple_wallet_data.0; |
| 549 | + let sek_key = tuple_wallet_data.1; |
490 | 550 |
|
491 | | - let result = match _wallet_scan_outputs( |
492 | | - wallet, |
493 | | - sek_key, |
494 | | - start_height, |
495 | | - number_of_blocks |
496 | | - ) { |
497 | | - Ok(scan) => { |
498 | | - scan |
499 | | - }, Err(e ) => { |
500 | | - let error_msg = format!("Error {}", &e.to_string()); |
501 | | - let error_msg_ptr = CString::new(error_msg).unwrap(); |
502 | | - let ptr = error_msg_ptr.as_ptr(); // Get a pointer to the underlaying memory for s |
503 | | - std::mem::forget(error_msg_ptr); |
| 551 | + ensure_wallet!(wlt, wallet); |
| 552 | + |
| 553 | + match _wallet_scan_outputs( |
| 554 | + wallet, |
| 555 | + sek_key, |
| 556 | + start_height, |
| 557 | + number_of_blocks |
| 558 | + ) { |
| 559 | + Ok(scan) => scan, |
| 560 | + Err(e) => { |
| 561 | + let error_msg = format!("Error {}", &e.to_string()); |
| 562 | + let error_msg_ptr = CString::new(error_msg).unwrap(); |
| 563 | + let ptr = error_msg_ptr.as_ptr(); |
| 564 | + std::mem::forget(error_msg_ptr); |
| 565 | + ptr |
| 566 | + } |
| 567 | + } |
| 568 | + })); |
| 569 | + match result { |
| 570 | + Ok(ptr) => ptr, |
| 571 | + Err(_) => { |
| 572 | + // Panic already captured by install_panic_hook; surface a Dart-side |
| 573 | + // error so the wallet's _startScans rethrows rather than the host |
| 574 | + // process aborting on the unwind boundary. |
| 575 | + let error_msg = CString::new( |
| 576 | + "Error scanOutputs panicked; see flutter_libmwc-panic.log", |
| 577 | + ) |
| 578 | + .unwrap(); |
| 579 | + let ptr = error_msg.as_ptr(); |
| 580 | + std::mem::forget(error_msg); |
504 | 581 | ptr |
505 | 582 | } |
506 | | - }; |
507 | | - result |
| 583 | + } |
508 | 584 | } |
509 | 585 |
|
510 | 586 | fn _wallet_scan_outputs( |
@@ -751,6 +827,7 @@ fn _tx_cancel( |
751 | 827 | pub unsafe extern "C" fn mwc_rust_get_chain_height( |
752 | 828 | config: *const c_char, |
753 | 829 | ) -> *const c_char { |
| 830 | + install_panic_hook(); |
754 | 831 | let result = match _get_chain_height( |
755 | 832 | config |
756 | 833 | ) { |
@@ -1376,6 +1453,15 @@ pub fn get_chain_height(config: &str) -> Result<u64, Error> { |
1376 | 1453 | return Err(e); |
1377 | 1454 | } |
1378 | 1455 | }; |
| 1456 | + // HTTPNodeClient::get_chain_tip touches MWC code paths that require the |
| 1457 | + // chain type to be set. get_chain_height can run before any wallet-open |
| 1458 | + // path on a fresh process (e.g. via Stack Wallet's chainHeight getter |
| 1459 | + // firing on shouldAutoSync), so we must initialize chain type here too. |
| 1460 | + let target_chaintype = wallet_config.chain_type.unwrap_or(ChainTypes::Mainnet); |
| 1461 | + global::set_global_chain_type(target_chaintype); |
| 1462 | + if global::get_chain_type() != target_chaintype { |
| 1463 | + global::set_local_chain_type(target_chaintype); |
| 1464 | + }; |
1379 | 1465 | let node_api_secret = get_first_line(wallet_config.node_api_secret_path.clone()); |
1380 | 1466 | let node_client = HTTPNodeClient::new(vec![wallet_config.check_node_api_http_addr], node_api_secret) |
1381 | 1467 | .map_err(|e| Error::ClientCallback(format!("{}", e)))?; |
@@ -1902,6 +1988,18 @@ impl Task for Listener { |
1902 | 1988 | type Output = usize; |
1903 | 1989 |
|
1904 | 1990 | fn run(&self, cancel_tok: &CancellationToken) -> Result<Self::Output, anyhow::Error> { |
| 1991 | + install_panic_hook(); |
| 1992 | + // The listener runs on a worker thread spawned by `export_task!`. |
| 1993 | + // `mwc_rust_open_wallet` set `GLOBAL_CHAIN_TYPE` on the main thread, |
| 1994 | + // but the worker has its own thread-local `CHAIN_TYPE`. MWC code that |
| 1995 | + // reads `get_chain_type()` falls back to the global, so this is a |
| 1996 | + // defensive belt-and-suspenders init for any path that might read the |
| 1997 | + // thread-local directly. |
| 1998 | + let target_chaintype = ChainTypes::Mainnet; |
| 1999 | + global::set_global_chain_type(target_chaintype); |
| 2000 | + if global::get_chain_type() != target_chaintype { |
| 2001 | + global::set_local_chain_type(target_chaintype); |
| 2002 | + } |
1905 | 2003 | let mut spins = 0; |
1906 | 2004 | let tuple_wallet_data: (u64, Option<SecretKey>) = serde_json::from_str(&self.wallet_ptr_str)?; |
1907 | 2005 | let wlt = tuple_wallet_data.0; |
@@ -1947,6 +2045,7 @@ pub unsafe extern "C" fn mwc_rust_mwcmqs_listener_start( |
1947 | 2045 | wallet: *const c_char, |
1948 | 2046 | mwcmqs_config: *const c_char, |
1949 | 2047 | ) -> *mut c_void { |
| 2048 | + install_panic_hook(); |
1950 | 2049 | let wallet_ptr = CStr::from_ptr(wallet); |
1951 | 2050 | let mwcmqs_config = CStr::from_ptr(mwcmqs_config); |
1952 | 2051 | let mwcmqs_config = mwcmqs_config.to_str().unwrap(); |
|
0 commit comments