Skip to main content

microsandbox_control_client/
connection.rs

1//! Automatic format discovery outside the generic framed router.
2
3use std::path::Path;
4use std::sync::Arc;
5
6use microsandbox_protocol::control::DEFAULT_REQUEST_TIMEOUT;
7use microsandbox_protocol_client::{
8    ClientError, ConnectOptions, Connector, Delivery, ErrorKind, LocalConnector, Message, Protocol,
9    RequestOptions,
10};
11use tokio::time::timeout_at;
12use tokio_util::sync::CancellationToken;
13
14use crate::{
15    CompatibleControlRequest, ControlClient, ControlClientError, ControlClientResult,
16    ControlProtocol, IntoControlMessage, JsonControlClient, JsonReply, VerifiedControlConnector,
17    dialer::Dialer,
18    json_client::{check_deadline, deadline},
19};
20
21//--------------------------------------------------------------------------------------------------
22// Types
23//--------------------------------------------------------------------------------------------------
24
25/// Operation format selected during this connection's setup.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ControlMode {
28    /// Persistent, negotiated CBOR frames.
29    Framed,
30    /// Legacy JSON unary exchanges.
31    Json,
32}
33
34/// The actual reply format. JSON never receives synthetic IDs or CBOR bytes.
35#[derive(Debug)]
36pub enum ControlReply {
37    /// Original framed response, including unknown fields.
38    Framed(Message),
39    /// Original JSON line and its lossless inspection view.
40    Json(JsonReply),
41}
42
43/// Shared discovery result and operation adapter. Standalone JSON connections
44/// rediscover before fresh exchanges unless a verified runtime owner is supplied.
45#[derive(Clone)]
46pub struct ControlConnection {
47    inner: Arc<Inner>,
48}
49
50struct Inner {
51    selected: Selected,
52    dialer: Dialer,
53    options: ConnectOptions,
54    closed: CancellationToken,
55    capabilities: crate::Capabilities,
56    runtime_capabilities: crate::RuntimeCapabilities,
57}
58
59enum Selected {
60    Framed(ControlClient),
61    Json(JsonControlClient),
62}
63
64//--------------------------------------------------------------------------------------------------
65// Methods
66//--------------------------------------------------------------------------------------------------
67
68impl ControlConnection {
69    /// Discover support at an existing native endpoint. Path identity alone is
70    /// not sufficient to reuse JSON format evidence across process replacement.
71    pub async fn connect(path: impl AsRef<Path>) -> ControlClientResult<Self> {
72        Self::connect_with(path, |options| options).await
73    }
74
75    /// Configure one total deadline covering discovery, redial, and handshake.
76    pub async fn connect_with(
77        path: impl AsRef<Path>,
78        configure: impl FnOnce(ConnectOptions) -> ConnectOptions,
79    ) -> ControlClientResult<Self> {
80        Self::connect_connector_with(Arc::new(LocalConnector::new(path)), configure).await
81    }
82
83    /// Discover using a repeatable, owned connector without claimed OS identity.
84    pub async fn connect_connector(connector: Arc<dyn Connector>) -> ControlClientResult<Self> {
85        Self::connect_connector_with(connector, |options| options).await
86    }
87
88    /// Configure discovery over caller-provided independent transports.
89    pub async fn connect_connector_with(
90        connector: Arc<dyn Connector>,
91        configure: impl FnOnce(ConnectOptions) -> ConnectOptions,
92    ) -> ControlClientResult<Self> {
93        Self::establish(
94            Dialer::Unverified(connector),
95            configure(ConnectOptions::default()),
96        )
97        .await
98    }
99
100    /// Reuse JSON discovery while an external runtime owner verifies every peer
101    /// and rechecks its active run and process birth before each operation.
102    pub async fn connect_verified_connector(
103        connector: Arc<dyn VerifiedControlConnector>,
104    ) -> ControlClientResult<Self> {
105        Self::connect_verified_connector_with(connector, |options| options).await
106    }
107
108    /// Configure setup for an identity-verifying backend connector.
109    pub async fn connect_verified_connector_with(
110        connector: Arc<dyn VerifiedControlConnector>,
111        configure: impl FnOnce(ConnectOptions) -> ConnectOptions,
112    ) -> ControlClientResult<Self> {
113        Self::establish(
114            Dialer::Verified(connector),
115            configure(ConnectOptions::default()),
116        )
117        .await
118    }
119
120    async fn establish(dialer: Dialer, options: ConnectOptions) -> ControlClientResult<Self> {
121        options.limits.validate()?;
122        let until = deadline(options.setup_timeout)?;
123        timeout_at(until, async {
124            let json = JsonControlClient::configured(
125                dialer.clone(),
126                options.clone(),
127                Some(ControlMode::Json),
128            );
129            let (mode, runtime_capabilities) = json.discover(until).await?;
130            let capabilities = runtime_capabilities.generation_one();
131            let selected = match mode {
132                ControlMode::Json => Selected::Json(json),
133                ControlMode::Framed => {
134                    // JSON completes on its own stream. A positively advertised
135                    // framed path gets a fresh connection, never an in-place
136                    // upgrade or a fallback after a failed welcome.
137                    let transport = dialer.connect(until).await?;
138                    dialer.verify(until).await?;
139                    check_deadline(until)?;
140                    let established =
141                        ControlProtocol::establish(transport, options.clone()).await?;
142                    Selected::Framed(ControlClient::from_established(established).await?)
143                }
144            };
145            Ok(Self {
146                inner: Arc::new(Inner {
147                    selected,
148                    dialer,
149                    options,
150                    closed: CancellationToken::new(),
151                    capabilities,
152                    runtime_capabilities,
153                }),
154            })
155        })
156        .await
157        .unwrap_or_else(|_| Err(ClientError::new(ErrorKind::Timeout).into()))
158        .map_err(not_sent)
159    }
160
161    /// Format selected by this setup. A later detected format change invalidates
162    /// the JSON handle before sending; callers explicitly establish a new one.
163    pub fn mode(&self) -> ControlMode {
164        match self.inner.selected {
165            Selected::Framed(_) => ControlMode::Framed,
166            Selected::Json(_) => ControlMode::Json,
167        }
168    }
169
170    /// Capability snapshot validated during discovery. Reading it performs no
171    /// I/O; use GetCapabilities to explicitly request a fresh observation.
172    pub fn capabilities(&self) -> &crate::Capabilities {
173        &self.inner.capabilities
174    }
175
176    /// Complete capability snapshot discovered through the compatibility endpoint.
177    pub fn runtime_capabilities(&self) -> &crate::RuntimeCapabilities {
178        &self.inner.runtime_capabilities
179    }
180
181    /// Inspect shared closure without dialing or rediscovery.
182    pub fn is_closed(&self) -> bool {
183        match &self.inner.selected {
184            Selected::Framed(client) => client.is_closed(),
185            Selected::Json(client) => client.is_closed(),
186        }
187    }
188
189    /// Wait for the selected connection to close without dialing or polling.
190    pub async fn closed(&self) {
191        match &self.inner.selected {
192            Selected::Framed(client) => client.closed().await,
193            Selected::Json(client) => client.closed().await,
194        }
195    }
196
197    /// Obtain the complete generic framed surface. JSON fails locally, without
198    /// opening an endpoint or manufacturing an equivalent frame.
199    pub fn framed(&self) -> ControlClientResult<&ControlClient> {
200        match &self.inner.selected {
201            Selected::Framed(client) => Ok(client),
202            Selected::Json(_) => Err(ControlClientError::UnsupportedMode),
203        }
204    }
205
206    /// Close all shared handles. Subsequent requests cannot silently reconnect.
207    pub async fn close(&self) {
208        // Verification can be awaiting a backend/database operation before the
209        // generic client has a request to wake. It shares explicit close too.
210        self.inner.closed.cancel();
211        match &self.inner.selected {
212            Selected::Framed(client) => client.close().await,
213            Selected::Json(client) => client.close().await,
214        }
215    }
216
217    /// Send a named request and retain its actual reply representation.
218    pub async fn request(
219        &self,
220        message: impl IntoControlMessage,
221    ) -> ControlClientResult<ControlReply> {
222        self.request_with(message, |options| options).await
223    }
224
225    /// Configure one attempt, including any identity recheck or JSON rediscovery.
226    pub async fn request_with(
227        &self,
228        message: impl IntoControlMessage,
229        configure: impl FnOnce(RequestOptions) -> RequestOptions,
230    ) -> ControlClientResult<ControlReply> {
231        let options = configure(RequestOptions::default());
232        match &self.inner.selected {
233            Selected::Json(client) => Ok(ControlReply::Json(
234                client.operation(message.into_json()?, options).await?,
235            )),
236            Selected::Framed(client) => {
237                let options = self.verify_before_request(options).await?;
238                Ok(ControlReply::Framed(
239                    client.request_with(message, |_| options).await?,
240                ))
241            }
242        }
243    }
244
245    /// Normalize a checked request using the selected format's real decoder.
246    pub async fn request_typed<R: CompatibleControlRequest>(
247        &self,
248        request: &R,
249    ) -> ControlClientResult<R::Response> {
250        self.request_typed_with(request, |options| options).await
251    }
252
253    /// Configure one checked operation's local wait.
254    pub async fn request_typed_with<R: CompatibleControlRequest>(
255        &self,
256        request: &R,
257        configure: impl FnOnce(RequestOptions) -> RequestOptions,
258    ) -> ControlClientResult<R::Response> {
259        let options = configure(RequestOptions::default());
260        match &self.inner.selected {
261            Selected::Json(client) => client.request_compatible(request, options).await,
262            Selected::Framed(client) => {
263                if request.min_generation() > client.ready().welcome.generation {
264                    return self.inner_json(request, options).await;
265                }
266                let options = self.verify_before_request(options).await?;
267                client.request_typed_with(request, |_| options).await
268            }
269        }
270    }
271
272    async fn inner_json<R: CompatibleControlRequest>(
273        &self,
274        request: &R,
275        options: RequestOptions,
276    ) -> ControlClientResult<R::Response> {
277        // The operation is selected before any mutation bytes are sent. This is a generation-one
278        // compatibility route, never a retry after a framed failure.
279        let json = JsonControlClient::configured(
280            self.inner.dialer.clone(),
281            self.inner.options.clone(),
282            Some(ControlMode::Framed),
283        );
284        json.request_compatible(request, options).await
285    }
286
287    async fn verify_before_request(
288        &self,
289        options: RequestOptions,
290    ) -> ControlClientResult<RequestOptions> {
291        if self.is_closed() {
292            return Err(ClientError::new(ErrorKind::Closed).into());
293        }
294        let until = deadline(
295            options
296                .request_timeout
297                .or(self.inner.options.limits.request_timeout)
298                .unwrap_or(DEFAULT_REQUEST_TIMEOUT),
299        )?;
300        check_deadline(until)?;
301        let result = tokio::select! {
302            biased;
303            _ = self.inner.closed.cancelled() => Err(ClientError::new(ErrorKind::Closed).into()),
304            result = timeout_at(until, self.inner.dialer.verify(until)) => result
305                .unwrap_or_else(|_| Err(ClientError::new(ErrorKind::Timeout).into())),
306        };
307        if let Err(error) = result {
308            self.close().await;
309            return Err(not_sent(error));
310        }
311        check_deadline(until)?;
312        Ok(RequestOptions::default()
313            .request_timeout(until.saturating_duration_since(tokio::time::Instant::now())))
314    }
315}
316
317//--------------------------------------------------------------------------------------------------
318// Functions
319//--------------------------------------------------------------------------------------------------
320
321pub(crate) fn not_sent(error: ControlClientError) -> ControlClientError {
322    match error {
323        ControlClientError::Client(error) => error.with_delivery(Delivery::NotSent).into(),
324        error => error,
325    }
326}