dittolive_ditto/ditto/init/
mod.rs1use std::{
2 ffi::CString,
3 future::Future,
4 os::raw::{c_uint, c_void},
5 str::FromStr,
6 sync::{Arc, Weak},
7 time::Duration,
8};
9
10use async_fn_traits::AsyncFn2;
11use async_trait::async_trait;
12pub(crate) use config_or_identity::ConfigOrIdentity;
13use extern_c::extern_c;
14use ffi_sdk::{ffi_utils::Blocking, FsComponent, TransportConfigMode};
15
16pub use self::config::{DittoConfig, DittoConfigConnect};
17use crate::{
18 ditto::DittoFields,
19 identity::DittoAuthenticator,
20 small_peer_info::SmallPeerInfo,
21 utils::{make_continuation, prelude::*},
22};
23
24mod config;
25mod config_or_identity;
26
27impl Ditto {
28 pub async fn open(config: DittoConfig) -> Result<Ditto, DittoError> {
53 let config_cbor =
54 serde_cbor::to_vec(&config).expect("should serialize well-formed DittoConfig");
55 let config_cbor_ref = (&*config_cbor).into();
56 let (continuation, recv) = make_continuation();
57 let default_root_dir = default_root_directory();
58 let default_root_dir_ref = default_root_dir
59 .as_deref()
60 .and_then(|root| root.to_str())
61 .ok_or_else(|| {
62 DittoError::new(
63 ErrorKind::IO,
64 "Unable to resolve a default data directory on this platform".to_string(),
65 )
66 })?;
67 let default_root_dir_cstring = CString::from_str(default_root_dir_ref)
68 .expect("should construct CString from no-nulls &str");
69 let default_root_dir_cstr = &*default_root_dir_cstring;
70 let default_root_dir_charp: char_p::Ref<'_> = default_root_dir_cstr.into();
71
72 #[allow(clippy::useless_conversion)]
74 ffi_sdk::dittoffi_ditto_open_async_throws(
75 config_cbor_ref,
76 TransportConfigMode::PlatformIndependent,
77 default_root_dir_charp,
78 continuation.into(),
79 );
80
81 let ffi_result = recv.await.unwrap();
82 let ffi_ditto: repr_c::Box<ffi_sdk::Ditto> = ffi_result.into_rust_result()?;
83
84 Self::finish_open(ffi_ditto, config)
85 }
86
87 pub fn open_sync(config: DittoConfig) -> Result<Ditto, DittoError> {
115 let config_cbor =
116 serde_cbor::to_vec(&config).expect("should serialize well-formed DittoConfig");
117 let config_cbor_ref = (&(*config_cbor)).into();
118
119 let default_root_dir = default_root_directory();
120 let default_root_dir_ref = default_root_dir
121 .as_deref()
122 .and_then(|root| root.to_str())
123 .ok_or_else(|| {
124 DittoError::new(
125 ErrorKind::IO,
126 "Unable to resolve a default data directory on this platform".to_string(),
127 )
128 })?;
129 let default_root_dir_cstring = CString::from_str(default_root_dir_ref)
130 .expect("should construct CString from no-nulls &str");
131 let default_root_dir_cstr = &*default_root_dir_cstring;
132 let default_root_dir_charp: char_p::Ref<'_> = default_root_dir_cstr.into();
133
134 let ffi_ditto: repr_c::Box<ffi_sdk::Ditto> = ffi_sdk::dittoffi_ditto_open_throws(
135 config_cbor_ref,
136 TransportConfigMode::PlatformIndependent,
137 default_root_dir_charp,
138 )
139 .into_rust_result()?;
140
141 Self::finish_open(ffi_ditto, config)
142 }
143
144 fn finish_open(
145 ffi_ditto: repr_c::Box<ffi_sdk::Ditto>,
146 config: DittoConfig,
147 ) -> Result<Ditto, DittoError> {
148 let ditto: Arc<repr_c::Box<ffi_sdk::Ditto>> = Arc::new(ffi_ditto);
149 let has_auth = matches!(&config.connect, DittoConfigConnect::Server { .. });
150 #[allow(deprecated)]
151 let site_id: SiteId = ffi_sdk::ditto_auth_client_get_site_id(&ditto);
152
153 let ditto_root = {
154 let ditto_root = if let Some(dir) = &config.persistence_directory {
155 PersistentRoot::new(dir)?
156 } else {
157 let default_dir = default_root_directory().ok_or_else(|| {
158 DittoError::new(
159 ErrorKind::IO,
160 "failed to determine default Ditto persistence directory",
161 )
162 })?;
163 PersistentRoot::new(default_dir)?
164 };
165 Arc::new(ditto_root) as Arc<dyn DittoRoot>
166 };
167 let disk_usage = DiskUsage::new(ditto.retain(), FsComponent::Root);
168 let small_peer_info = SmallPeerInfo::new(ditto.retain());
169 let fields = Arc::new_cyclic(|weak_fields: &arc::Weak<_>| {
170 let store = Store::new(ditto.retain(), weak_fields.clone());
171 let sync = crate::sync::Sync::new(weak_fields.clone());
172 let presence = Arc::new(Presence::new(weak_fields.clone()));
173
174 DittoFields {
175 ditto: ditto.retain(),
176 has_auth,
177 state: ConfigOrIdentity::Config(config),
178 store,
179 sync,
180 site_id,
181 ditto_root,
182 presence,
183 disk_usage,
184 small_peer_info,
185 }
186 });
187
188 let ditto = Ditto {
189 fields,
190 is_shut_down_able: true,
191 };
192
193 Ok(ditto)
194 }
195}
196
197fn default_root_directory() -> Option<PathBuf> {
198 std::env::current_exe()
199 .ok()
200 .and_then(|abspath| abspath.parent().map(|x| x.to_path_buf()))
201}
202
203impl DittoAuthenticator {
204 pub fn set_expiration_handler<F>(&self, handler: F)
253 where
254 F: DittoAuthExpirationHandler,
255 {
256 let Some(ditto) = self.ditto_fields.upgrade() else {
257 error!("Failed to set expiration handler, Ditto has shut down");
258 return;
259 };
260
261 let login_provider = make_login_provider(self.ditto_fields.clone(), Arc::new(handler));
262 ffi_sdk::ditto_auth_set_login_provider(&ditto.ditto, Some(login_provider));
263 }
264
265 pub fn clear_expiration_handler(&self) {
267 let Some(ditto) = self.ditto_fields.upgrade() else {
268 error!("Failed to clear expiration handler, Ditto has shut down");
269 return;
270 };
271
272 ffi_sdk::ditto_auth_set_login_provider(&ditto.ditto, None);
273 }
274}
275
276pub trait DittoAuthExpirationHandler: 'static + Send + Sync {
324 fn on_expiration(
325 &self,
326 ditto: &Ditto,
327 duration_remaining: Duration,
328 ) -> impl Send + Future<Output = ()>;
329}
330
331impl<F> DittoAuthExpirationHandler for F
332where
333 F: 'static + Send + Sync,
334 F: for<'r> AsyncFn2<&'r Ditto, Duration, Output = (), OutputFuture: Send>,
335{
336 async fn on_expiration(&self, ditto: &Ditto, duration_remaining: Duration) {
337 self(ditto, duration_remaining).await
338 }
339}
340
341#[async_trait]
342pub(crate) trait DynDittoAuthExpirationHandler: 'static + Send + Sync {
343 async fn dyn_on_expiration(&self, ditto: &Ditto, duration_remaining: Duration);
344}
345
346#[async_trait]
347impl<F: DittoAuthExpirationHandler> DynDittoAuthExpirationHandler for F {
348 async fn dyn_on_expiration(&self, ditto: &Ditto, duration_remaining: Duration) {
349 self.on_expiration(ditto, duration_remaining).await
350 }
351}
352
353impl DittoAuthExpirationHandler for dyn '_ + DynDittoAuthExpirationHandler {
354 async fn on_expiration(&self, ditto: &Ditto, duration_remaining: Duration) {
355 self.dyn_on_expiration(ditto, duration_remaining).await
356 }
357}
358
359pub(crate) fn make_login_provider(
360 ditto_fields: Weak<DittoFields>,
361 auth_expiration_handler: Arc<dyn DynDittoAuthExpirationHandler>,
362) -> repr_c::Box<ffi_sdk::LoginProvider> {
363 struct LoginProviderCtx {
364 ditto_fields: Weak<DittoFields>,
365 auth_expiration_handler: Arc<dyn DynDittoAuthExpirationHandler>,
366 }
367
368 let login_provider_ctx = Arc::new(LoginProviderCtx {
369 auth_expiration_handler,
370 ditto_fields,
371 });
372
373 let ffi_ctx = Arc::as_ptr(&login_provider_ctx) as *mut c_void;
374 let ffi_retain = Some(extern_c(|ctx: *mut c_void, _: Blocking| unsafe {
375 Arc::<LoginProviderCtx>::increment_strong_count(ctx.cast())
376 }) as _);
377 let ffi_release = Some(extern_c(|ctx: *mut c_void, _: Blocking| unsafe {
378 Arc::<LoginProviderCtx>::decrement_strong_count(ctx.cast())
379 }) as _);
380
381 let ffi_handler = extern_c(|ctx: *mut c_void, secs_remaining: c_uint, _: Blocking| {
384 let login_provider_ctx: &LoginProviderCtx = unsafe { &*ctx.cast() };
385 let auth_expiration_handler = login_provider_ctx.auth_expiration_handler.retain();
386 let Ok(ditto) = Ditto::upgrade(&login_provider_ctx.ditto_fields) else {
387 error!("Failed to dispatch auth handler, Ditto has been shut down");
388 return;
389 };
390
391 dispatch_auth_handler(
392 auth_expiration_handler,
393 ditto,
394 Duration::from_secs(secs_remaining.into()),
395 );
396 });
397
398 unsafe {
399 ffi_sdk::ditto_auth_client_make_login_provider(
400 ffi_ctx,
401 ffi_retain,
402 ffi_release,
403 ffi_handler,
404 )
405 }
406}
407
408fn dispatch_auth_handler(
423 auth_expiration_handler: Arc<dyn DynDittoAuthExpirationHandler>,
424 ditto: Ditto,
425 duration_remaining: Duration,
426) {
427 match tokio::runtime::Handle::try_current() {
429 Ok(handle) => {
430 handle.spawn(async move {
432 auth_expiration_handler
433 .on_expiration(&ditto, duration_remaining)
434 .await;
435 });
436 }
437 Err(_) => {
438 warn!("No tokio runtime available for expiration handler. Creating temporary runtime.");
440
441 match tokio::runtime::Builder::new_current_thread()
442 .enable_all()
443 .build()
444 {
445 Ok(rt) => {
446 rt.block_on(async move {
447 auth_expiration_handler
448 .on_expiration(&ditto, duration_remaining)
449 .await;
450 });
451 }
452 Err(e) => {
453 panic!(
454 "Failed to create tokio runtime for expiration handler: {}. Consider \
455 running within a tokio runtime context.",
456 e
457 );
458 }
459 }
460 }
461 }
462}
463
464#[cfg(test)]
465mod tests {
466
467 #[test]
468 fn test_runtime_detection_behavior() {
469 println!("Testing runtime detection...");
471
472 match tokio::runtime::Handle::try_current() {
474 Ok(_handle) => {
475 println!("✓ Tokio runtime detected - handlers will be spawned");
476 }
477 Err(_) => {
478 println!("✗ No tokio runtime - will create temporary runtime");
479
480 match tokio::runtime::Builder::new_current_thread()
482 .enable_all()
483 .build()
484 {
485 Ok(_rt) => {
486 println!("✓ Successfully created temporary runtime");
487 }
488 Err(e) => {
489 println!("✗ Failed to create temporary runtime: {}", e);
490 }
491 }
492 }
493 }
494 }
495
496 #[tokio::test]
497 async fn test_with_tokio_runtime_available() {
498 println!("Testing with tokio runtime available...");
500
501 match tokio::runtime::Handle::try_current() {
503 Ok(_handle) => {
504 println!("✓ Tokio runtime is available for async handlers");
505 }
506 Err(_) => {
507 panic!("Expected tokio runtime to be available in tokio::test");
508 }
509 }
510
511 println!("✓ Runtime detection works correctly in async context");
512 }
513
514 #[test]
515 fn test_improved_error_handling() {
516 let rt_result = tokio::runtime::Builder::new_current_thread()
518 .enable_all()
519 .build();
520
521 match rt_result {
522 Ok(rt) => {
523 println!("✓ Temporary runtime creation works");
524
525 rt.block_on(async {
527 println!("✓ Async work executes successfully in temporary runtime");
528 });
529 }
530 Err(e) => {
531 println!("✗ Failed to create runtime: {}", e);
532 }
533 }
534 }
535}