field_lib

Cross-platform Rust runtime for Radroots iOS and Android apps
git clone https://radroots.dev/git/field_lib.git
Log | Files | Refs | README | LICENSE

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 }