diff --git a/Cargo.lock b/Cargo.lock index f6bd6a24ae..61be2df95f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16842,6 +16842,7 @@ dependencies = [ name = "tauri-plugin-apple-calendar" version = "0.1.0" dependencies = [ + "backon", "block2", "chrono", "itertools 0.14.0", diff --git a/Cargo.toml b/Cargo.toml index 935b6ac212..6bbf138a6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -169,7 +169,7 @@ tokio-util = "0.7.15" anyhow = "1" approx = "0.5.1" -backon = "1.5.2" +backon = "1.6.0" base64 = "0.22.1" bytes = "1.9.0" cached = "0.55.1" diff --git a/plugins/apple-calendar/Cargo.toml b/plugins/apple-calendar/Cargo.toml index 2adf2fe887..e9951a34cf 100644 --- a/plugins/apple-calendar/Cargo.toml +++ b/plugins/apple-calendar/Cargo.toml @@ -21,6 +21,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } specta = { workspace = true, features = ["chrono"] } +backon = { workspace = true, features = ["std-blocking-sleep"] } chrono = { workspace = true, features = ["serde"] } itertools = { workspace = true } thiserror = { workspace = true } diff --git a/plugins/apple-calendar/src/apple/handle.rs b/plugins/apple-calendar/src/apple/handle.rs index bd15df5506..ea73ad050b 100644 --- a/plugins/apple-calendar/src/apple/handle.rs +++ b/plugins/apple-calendar/src/apple/handle.rs @@ -1,3 +1,7 @@ +use std::panic::AssertUnwindSafe; +use std::time::Duration; + +use backon::{BlockingRetryable, ConstantBuilder}; use itertools::Itertools; use objc2::{AllocAnyThread, rc::Retained}; use objc2_event_kit::{EKAuthorizationStatus, EKCalendar, EKEntityType, EKEvent, EKEventStore}; @@ -9,31 +13,38 @@ use crate::types::{AppleCalendar, AppleEvent}; use super::transforms::{transform_calendar, transform_event}; -pub struct Handle { - event_store: Retained, +fn retry_backoff() -> ConstantBuilder { + ConstantBuilder::default() + .with_delay(Duration::from_millis(100)) + .with_max_times(3) } -impl Default for Handle { - fn default() -> Self { - let event_store = unsafe { EKEventStore::new() }; - Self { event_store } +pub struct Handle; + +impl Handle { + fn create_event_store() -> Retained { + unsafe { EKEventStore::new() } } } impl Handle { - fn has_calendar_access(&self) -> bool { + fn has_calendar_access() -> bool { let status = unsafe { EKEventStore::authorizationStatusForEntityType(EKEntityType::Event) }; matches!(status, EKAuthorizationStatus::FullAccess) } - fn fetch_events(&self, filter: &EventFilter) -> Result>, Error> { - let calendars: Retained> = unsafe { self.event_store.calendars() } - .into_iter() - .filter(|c| { - let id = unsafe { c.calendarIdentifier() }.to_string(); - filter.calendar_tracking_id.eq(&id) - }) - .collect(); + fn fetch_events( + event_store: &EKEventStore, + filter: &EventFilter, + ) -> Result>, Error> { + let calendars: Retained> = + Self::get_calendars_with_exception_handling(event_store)? + .into_iter() + .filter(|c| { + let id = unsafe { c.calendarIdentifier() }.to_string(); + filter.calendar_tracking_id.eq(&id) + }) + .collect(); if calendars.is_empty() { return Err(Error::CalendarNotFound); @@ -50,58 +61,84 @@ impl Handle { .collect_tuple() .ok_or_else(|| Error::InvalidDateRange)?; - let predicate = unsafe { - self.event_store - .predicateForEventsWithStartDate_endDate_calendars( - &start_date, - &end_date, - Some(&calendars), - ) - }; + let event_store = AssertUnwindSafe(event_store); + let calendars = AssertUnwindSafe(calendars); + let start_date = AssertUnwindSafe(start_date); + let end_date = AssertUnwindSafe(end_date); + + let result = objc2::exception::catch(|| unsafe { + let predicate = event_store.predicateForEventsWithStartDate_endDate_calendars( + &start_date, + &end_date, + Some(&calendars), + ); + event_store.eventsMatchingPredicate(&predicate) + }); + + result.map_err(|_| Error::XpcConnectionFailed) + } - Ok(unsafe { self.event_store.eventsMatchingPredicate(&predicate) }) + fn get_calendars_with_exception_handling( + event_store: &EKEventStore, + ) -> Result>, Error> { + let event_store = AssertUnwindSafe(event_store); + objc2::exception::catch(|| unsafe { event_store.calendars() }) + .map_err(|_| Error::XpcConnectionFailed) } pub fn list_calendars(&self) -> Result, Error> { - if !self.has_calendar_access() { + if !Self::has_calendar_access() { return Err(Error::CalendarAccessDenied); } - let calendars = unsafe { self.event_store.calendars() }; - - let list = calendars - .iter() - .map(|calendar| transform_calendar(&calendar)) - .sorted_by(|a, b| a.title.cmp(&b.title)) - .collect(); + let fetch = || { + let event_store = Self::create_event_store(); + let calendars = Self::get_calendars_with_exception_handling(&event_store)?; + let list = calendars + .iter() + .map(|calendar| transform_calendar(&calendar)) + .sorted_by(|a, b| a.title.cmp(&b.title)) + .collect(); + Ok(list) + }; - Ok(list) + fetch + .retry(retry_backoff()) + .when(|e| matches!(e, Error::XpcConnectionFailed)) + .call() } pub fn list_events(&self, filter: EventFilter) -> Result, Error> { - if !self.has_calendar_access() { + if !Self::has_calendar_access() { return Err(Error::CalendarAccessDenied); } - let events_array = self.fetch_events(&filter)?; + let fetch = || { + let event_store = Self::create_event_store(); + let events_array = Self::fetch_events(&event_store, &filter)?; - let events: Result, _> = events_array - .iter() - .filter_map(|event| { - let calendar = unsafe { event.calendar() }?; - let calendar_id = unsafe { calendar.calendarIdentifier() }; + let events: Result, _> = events_array + .iter() + .filter_map(|event| { + let calendar = unsafe { event.calendar() }?; + let calendar_id = unsafe { calendar.calendarIdentifier() }; - if !filter.calendar_tracking_id.eq(&calendar_id.to_string()) { - return None; - } + if !filter.calendar_tracking_id.eq(&calendar_id.to_string()) { + return None; + } - Some(transform_event(&event)) - }) - .collect(); + Some(transform_event(&event)) + }) + .collect(); - let mut events = events?; - events.sort_by(|a, b| a.start_date.cmp(&b.start_date)); + let mut events = events?; + events.sort_by(|a, b| a.start_date.cmp(&b.start_date)); + Ok(events) + }; - Ok(events) + fetch + .retry(retry_backoff()) + .when(|e| matches!(e, Error::XpcConnectionFailed)) + .call() } } diff --git a/plugins/apple-calendar/src/error.rs b/plugins/apple-calendar/src/error.rs index 70ecad49c1..c9cd2097ea 100644 --- a/plugins/apple-calendar/src/error.rs +++ b/plugins/apple-calendar/src/error.rs @@ -16,6 +16,8 @@ pub enum Error { InvalidDateRange, #[error("objective-c exception: {0}")] ObjectiveCException(String), + #[error("xpc connection failed")] + XpcConnectionFailed, #[error("transform error: {0}")] TransformError(String), #[error("permission denied: {0}")] diff --git a/plugins/apple-calendar/src/ext.rs b/plugins/apple-calendar/src/ext.rs index b1883350c0..c0b7e5be2d 100644 --- a/plugins/apple-calendar/src/ext.rs +++ b/plugins/apple-calendar/src/ext.rs @@ -34,13 +34,13 @@ impl<'a, R: tauri::Runtime, M: tauri::Manager> AppleCalendarExt<'a, R, M> { #[tracing::instrument(skip_all)] pub fn list_calendars(&self) -> Result, String> { - let handle = crate::apple::Handle::default(); + let handle = crate::apple::Handle; handle.list_calendars().map_err(|e| e.to_string()) } #[tracing::instrument(skip_all)] pub fn list_events(&self, filter: EventFilter) -> Result, String> { - let handle = crate::apple::Handle::default(); + let handle = crate::apple::Handle; handle.list_events(filter).map_err(|e| e.to_string()) } }