aster_forge_cloud_files_macos_bridge/
session.rs

1//! Extension-instance lifecycle and non-cloneable accepted-request leases.
2
3use std::{
4    fmt,
5    sync::{Arc, Mutex, MutexGuard},
6};
7
8use aster_forge_cloud_files_core::{SessionGeneration, SessionState};
9
10use crate::{MacosBridgeError, Result};
11
12#[derive(Debug)]
13struct SessionLifecycle {
14    state: SessionState,
15    active_requests: usize,
16}
17
18#[derive(Debug)]
19struct SessionInner {
20    generation: SessionGeneration,
21    lifecycle: Mutex<SessionLifecycle>,
22}
23
24/// Shareable lifecycle fence for one File Provider extension instance.
25#[derive(Debug, Clone)]
26pub struct MacosExtensionSession {
27    inner: Arc<SessionInner>,
28}
29
30impl MacosExtensionSession {
31    /// Starts one accepting extension generation.
32    #[must_use]
33    pub fn new(generation: SessionGeneration) -> Self {
34        Self {
35            inner: Arc::new(SessionInner {
36                generation,
37                lifecycle: Mutex::new(SessionLifecycle {
38                    state: SessionState::Accepting,
39                    active_requests: 0,
40                }),
41            }),
42        }
43    }
44
45    /// Returns the monotonic extension-instance generation.
46    #[must_use]
47    pub fn generation(&self) -> SessionGeneration {
48        self.inner.generation
49    }
50
51    /// Returns the current lifecycle state.
52    #[must_use]
53    pub fn state(&self) -> SessionState {
54        lock(&self.inner.lifecycle).state
55    }
56
57    /// Returns the number of accepted requests still owned by downstream work.
58    #[must_use]
59    pub fn active_requests(&self) -> usize {
60        lock(&self.inner.lifecycle).active_requests
61    }
62
63    pub(crate) fn into_ffi_handle(self) -> *const () {
64        Arc::into_raw(self.inner).cast()
65    }
66
67    pub(crate) unsafe fn clone_from_ffi_handle(handle: *const ()) -> Result<Self> {
68        if handle.is_null() {
69            return Err(MacosBridgeError::InvalidFfiInput {
70                reason: "extension session handle must not be null",
71            });
72        }
73        let pointer = handle.cast::<SessionInner>();
74        // SAFETY: the C ABI contract requires `handle` to be one live pointer returned by
75        // `into_ffi_handle`. Incrementing first creates an independent temporary Arc owner.
76        unsafe { Arc::increment_strong_count(pointer) };
77        // SAFETY: the increment above established one owned strong reference for this Arc.
78        let inner = unsafe { Arc::from_raw(pointer) };
79        Ok(Self { inner })
80    }
81
82    pub(crate) unsafe fn release_ffi_handle(handle: *const ()) {
83        if handle.is_null() {
84            return;
85        }
86        let pointer = handle.cast::<SessionInner>();
87        // SAFETY: the C ABI contract requires one live unreleased session handle returned by
88        // `into_ffi_handle`. Reconstructing and dropping consumes that exact owner once.
89        let session = unsafe { Arc::from_raw(pointer) };
90        drop(session);
91    }
92
93    /// Accepts one request only while the exact generation is accepting work.
94    /// # Errors
95    ///
96    /// Returns an error when validation fails or an underlying backend, store, or platform
97    /// operation fails.
98    pub fn begin_request(
99        &self,
100        callback_generation: SessionGeneration,
101    ) -> Result<MacosExtensionRequestLease> {
102        if callback_generation != self.inner.generation {
103            return Err(MacosBridgeError::StaleSessionGeneration {
104                expected: self.inner.generation.get(),
105                actual: callback_generation.get(),
106            });
107        }
108        let mut lifecycle = lock(&self.inner.lifecycle);
109        if lifecycle.state != SessionState::Accepting {
110            return Err(MacosBridgeError::SessionNotAccepting {
111                state: lifecycle.state,
112            });
113        }
114        lifecycle.active_requests = lifecycle
115            .active_requests
116            .checked_add(1)
117            .ok_or(MacosBridgeError::ActiveRequestCountOverflow)?;
118        drop(lifecycle);
119        Ok(MacosExtensionRequestLease {
120            session: self.inner.clone(),
121            released: false,
122        })
123    }
124
125    /// Moves `Accepting -> Closing` and rejects later request ingress.
126    #[must_use]
127    pub fn begin_closing(&self) -> bool {
128        let mut lifecycle = lock(&self.inner.lifecycle);
129        if lifecycle.state == SessionState::Accepting {
130            lifecycle.state = SessionState::Closing;
131            true
132        } else {
133            false
134        }
135    }
136
137    /// Marks the native extension/domain bridge disconnected and begins draining accepted work.
138    /// # Errors
139    ///
140    /// Returns an error when validation fails or an underlying backend, store, or platform
141    /// operation fails.
142    pub fn mark_disconnected(&self) -> Result<()> {
143        let mut lifecycle = lock(&self.inner.lifecycle);
144        match lifecycle.state {
145            SessionState::Closing => {
146                lifecycle.state = if lifecycle.active_requests == 0 {
147                    SessionState::Closed
148                } else {
149                    SessionState::Draining
150                };
151                Ok(())
152            }
153            SessionState::Draining | SessionState::Closed => Ok(()),
154            SessionState::Accepting => Err(MacosBridgeError::InvalidSessionTransition {
155                from: SessionState::Accepting,
156                to: SessionState::Draining,
157            }),
158        }
159    }
160}
161
162/// Non-cloneable ownership proof for one request accepted before closing.
163pub struct MacosExtensionRequestLease {
164    session: Arc<SessionInner>,
165    released: bool,
166}
167
168impl MacosExtensionRequestLease {
169    /// Returns the generation that accepted the request.
170    #[must_use]
171    pub fn generation(&self) -> SessionGeneration {
172        self.session.generation
173    }
174
175    /// Returns whether completion still belongs to the supplied extension instance.
176    #[must_use]
177    pub fn accepts_completion(&self, session: &MacosExtensionSession) -> bool {
178        !self.released
179            && Arc::ptr_eq(&self.session, &session.inner)
180            && lock(&self.session.lifecycle).state != SessionState::Closed
181    }
182
183    /// Releases the accepted request count explicitly.
184    pub fn release(mut self) {
185        self.release_inner();
186    }
187
188    pub(crate) fn into_ffi_handle(self) -> *mut () {
189        Box::into_raw(Box::new(self)).cast()
190    }
191
192    pub(crate) unsafe fn release_ffi_handle(handle: *mut ()) {
193        if handle.is_null() {
194            return;
195        }
196        // SAFETY: the C ABI contract requires one live unreleased request handle returned by
197        // `into_ffi_handle`. Reconstructing the Box consumes that exact allocation once.
198        let lease = unsafe { Box::from_raw(handle.cast::<Self>()) };
199        drop(lease);
200    }
201
202    fn release_inner(&mut self) {
203        if self.released {
204            return;
205        }
206        let mut lifecycle = lock(&self.session.lifecycle);
207        lifecycle.active_requests = lifecycle.active_requests.saturating_sub(1);
208        if lifecycle.state == SessionState::Draining && lifecycle.active_requests == 0 {
209            lifecycle.state = SessionState::Closed;
210        }
211        self.released = true;
212    }
213}
214
215impl fmt::Debug for MacosExtensionRequestLease {
216    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
217        formatter
218            .debug_struct("MacosExtensionRequestLease")
219            .field("generation", &self.session.generation)
220            .field("released", &self.released)
221            .finish()
222    }
223}
224
225impl Drop for MacosExtensionRequestLease {
226    fn drop(&mut self) {
227        self.release_inner();
228    }
229}
230
231fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
232    match mutex.lock() {
233        Ok(guard) => guard,
234        Err(poisoned) => poisoned.into_inner(),
235    }
236}