mod.rs (6643B)
1 pub mod app_info; 2 pub mod builder; 3 pub mod info; 4 pub mod key_management; 5 pub mod nostr; 6 #[cfg(feature = "nostr-client")] 7 pub mod trade_listing; 8 9 use chrono::Utc; 10 use radroots_net_core::{NetHandle, builder::NetBuilder}; 11 #[cfg(feature = "nostr-client")] 12 use std::sync::Mutex; 13 use std::sync::{ 14 RwLock, 15 atomic::{AtomicBool, Ordering}, 16 }; 17 #[cfg(feature = "nostr-client")] 18 use tokio::sync::broadcast::Receiver; 19 20 use self::{ 21 app_info::AppInfoPlatform, 22 info::{RuntimeInfo, gather_runtime_info}, 23 }; 24 use crate::RadrootsAppError; 25 26 #[derive(uniffi::Object)] 27 pub struct RadrootsRuntime { 28 pub(crate) net: NetHandle, 29 pub(crate) started_unix_ms: i64, 30 pub(crate) shutting_down: AtomicBool, 31 pub(crate) platform_app: RwLock<Option<AppInfoPlatform>>, 32 #[cfg(feature = "nostr-client")] 33 pub(crate) post_events_rx: Mutex< 34 Option< 35 Receiver< 36 radroots_events_codec::parsed::RadrootsParsedData< 37 radroots_events::post::RadrootsPost, 38 >, 39 >, 40 >, 41 >, 42 } 43 44 #[cfg_attr(not(coverage_nightly), uniffi::export)] 45 impl RadrootsRuntime { 46 #[cfg_attr(not(coverage_nightly), uniffi::constructor)] 47 pub fn new() -> Result<Self, RadrootsAppError> { 48 let cfg = radroots_net_core::config::NetConfig::default(); 49 #[cfg(feature = "rt")] 50 let handle = match NetBuilder::new().config(cfg).manage_runtime(true).build() { 51 Ok(handle) => handle, 52 Err(err) => { 53 return Err(RadrootsAppError::initialization(format!( 54 "net build failed: {err}" 55 ))); 56 } 57 }; 58 #[cfg(not(feature = "rt"))] 59 let handle = NetBuilder::new() 60 .config(cfg) 61 .manage_runtime(true) 62 .build() 63 .expect("net build must succeed when rt feature is disabled"); 64 65 Ok(Self { 66 net: handle, 67 started_unix_ms: Utc::now().timestamp_millis(), 68 shutting_down: AtomicBool::new(false), 69 platform_app: RwLock::new(None), 70 #[cfg(feature = "nostr-client")] 71 post_events_rx: Mutex::new(None), 72 }) 73 } 74 75 pub fn stop(&self) { 76 if self.shutting_down.swap(true, Ordering::SeqCst) { 77 let _ = crate::logging::log_info( 78 "Runtime stop already in progress or completed.".to_string(), 79 ); 80 return; 81 } 82 83 #[cfg(feature = "rt")] 84 { 85 if let Ok(mut net) = self.net.lock() { 86 if let Some(_rt) = net.rt.take() { 87 let _ = crate::logging::log_info("Runtime stopped gracefully.".to_string()); 88 } else { 89 let _ = crate::logging::log_info("No runtime was active at stop.".to_string()); 90 } 91 } else { 92 let _ = crate::logging::log_info( 93 "Failed to acquire runtime lock during stop.".to_string(), 94 ); 95 } 96 } 97 98 #[cfg(not(feature = "rt"))] 99 { 100 let _ = crate::logging::log_info( 101 "No managed runtime is available for this build.".to_string(), 102 ); 103 } 104 } 105 106 pub fn uptime_millis(&self) -> i64 { 107 Utc::now().timestamp_millis() - self.started_unix_ms 108 } 109 110 pub fn info(&self) -> RuntimeInfo { 111 gather_runtime_info(self) 112 } 113 114 pub fn info_json(&self) -> String { 115 #[cfg(feature = "rt")] 116 { 117 return match serde_json::to_string_pretty(&self.info()) { 118 Ok(json) => json, 119 Err(err) => format!(r#"{{"error":"serialize RuntimeInfo: {err}"}}"#), 120 }; 121 } 122 #[cfg(not(feature = "rt"))] 123 { 124 serde_json::to_string_pretty(&self.info()).unwrap_or_default() 125 } 126 } 127 128 pub fn set_app_info_platform( 129 &self, 130 platform: Option<String>, 131 bundle_id: Option<String>, 132 version: Option<String>, 133 build_number: Option<String>, 134 build_sha: Option<String>, 135 ) { 136 let platform_info = 137 AppInfoPlatform::new(platform, bundle_id, version, build_number, build_sha); 138 if let Ok(mut guard) = self.platform_app.write() { 139 *guard = Some(platform_info); 140 } 141 } 142 } 143 144 #[cfg(test)] 145 mod tests { 146 use super::RadrootsRuntime; 147 use std::panic::{AssertUnwindSafe, catch_unwind}; 148 149 fn init_info_logging() { 150 let _ = tracing_subscriber::fmt() 151 .with_test_writer() 152 .with_max_level(tracing::Level::INFO) 153 .try_init(); 154 } 155 156 fn poison_net_lock(runtime: &RadrootsRuntime) { 157 let handle = runtime.net.clone(); 158 let _ = catch_unwind(AssertUnwindSafe(|| { 159 let _guard = handle.lock().expect("lock net"); 160 panic!("poison net lock"); 161 })); 162 } 163 164 fn poison_platform_lock(runtime: &RadrootsRuntime) { 165 let _ = catch_unwind(AssertUnwindSafe(|| { 166 let _guard = runtime.platform_app.write().expect("lock platform"); 167 panic!("poison platform lock"); 168 })); 169 } 170 171 #[test] 172 fn runtime_info_uses_default_net_info_when_lock_is_poisoned() { 173 init_info_logging(); 174 let runtime = RadrootsRuntime::new().expect("runtime"); 175 176 let healthy = runtime.info(); 177 assert!(!healthy.net.crate_name.is_empty()); 178 poison_net_lock(&runtime); 179 180 let _ = runtime.uptime_millis(); 181 let info = runtime.info(); 182 assert_eq!(info.net.crate_name, String::new()); 183 assert_eq!(info.net.crate_version, String::new()); 184 let json = runtime.info_json(); 185 assert!(json.contains("\"net\"")); 186 runtime.stop(); 187 runtime.stop(); 188 } 189 190 #[test] 191 fn set_platform_info_handles_poisoned_lock() { 192 init_info_logging(); 193 let runtime = RadrootsRuntime::new().expect("runtime"); 194 runtime.set_app_info_platform( 195 Some("ios".to_string()), 196 Some("org.radroots.app".to_string()), 197 Some("1.0.0".to_string()), 198 Some("100".to_string()), 199 Some("abc123".to_string()), 200 ); 201 let info = runtime.info(); 202 assert_eq!( 203 info.app.platform.as_ref().and_then(|v| v.platform.clone()), 204 Some("ios".to_string()) 205 ); 206 poison_platform_lock(&runtime); 207 runtime.set_app_info_platform( 208 Some("ios".to_string()), 209 Some("org.radroots.app".to_string()), 210 Some("1.0.0".to_string()), 211 Some("100".to_string()), 212 Some("abc123".to_string()), 213 ); 214 } 215 }