Skip to main content

coreshift_core/
transport.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/
4
5//! Transport seam: commands, one-shot replies, and persistent watch sinks.
6//!
7//! This module is the message-shaped boundary between a daemon's
8//! transport-agnostic channel logic and its transport backends (the
9//! abstract-socket daemon today, a Binder backend later). The channel logic
10//! sees **commands and watchers**, never byte streams: each backend's only job
11//! is to turn its transport's native events into [`TransportEvent`]s and push
12//! structured update values into [`WatcherSink::push`].
13//!
14//! The seam is generic over the command and reply payload types (`C` and `R`);
15//! the channel logic (e.g. `FgCtx`/`FpsCtx`) provides the domain types and
16//! never knows which backend delivered them.
17//!
18//! ### Two shapes, kept distinct
19//!
20//! - [`ReplySink`] is **one-shot**: the single reply to a command, consumed
21//!   exactly once. The socket backend answers in-line (no hop); a Binder
22//!   backend unblocks a waiting transaction thread.
23//! - [`WatcherId`] / [`WatcherSink`] are **persistent**: a long-lived push
24//!   target with its own close / death lifecycle, outliving any single
25//!   transaction.
26//!
27//! Backends may implement both with the same underlying handle (a socket
28//! backend uses one fd for both roles over its lifetime); the seam does not
29//! assume that in general.
30//!
31//! ### Single-owner watcher registry
32//!
33//! A watch registration is itself a command, so it flows through the same
34//! ingestion path as `ping`/`status`. The reactor thread is therefore the only
35//! actor that ever allocates a [`WatcherId`]; [`TransportEvent::WatcherClosed`]
36//! and [`TransportEvent::WatcherDied`] flow back through the same path and are
37//! processed on that same thread.
38//!
39//! ### What ships here, and what ships with the Binder backend
40//!
41//! This module ships the message types only. The bounded per-channel ingestion
42//! queue + eventfd is Binder-backend machinery: only a Binder backend's
43//! `on_transact` fires on an arbitrary threadpool thread (a genuine cross-thread
44//! boundary), while the socket backend's thread already *is* the owning reactor
45//! thread and answers in-line with zero hop. The queue is added when the Binder
46//! backend lands, not as dead machinery before any consumer exists.
47
48use crate::CoreError;
49
50/// An opaque long-lived handle identifying one watch subscriber.
51///
52/// Allocated by the backend's owning thread; never interpreted by the channel
53/// logic. Backends should hand out monotonically increasing values from a
54/// per-loop counter.
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
56pub struct WatcherId(u64);
57
58impl WatcherId {
59    /// Construct a watcher id from a raw value.
60    ///
61    /// The caller (the backend's owning thread) is responsible for uniqueness
62    /// within its loop.
63    pub fn new(value: u64) -> Self {
64        Self(value)
65    }
66
67    /// The raw identifier value.
68    pub fn as_u64(&self) -> u64 {
69        self.0
70    }
71}
72
73/// Delivery closure backing a one-shot reply sink.
74type ReplySend<R> = Box<dyn FnOnce(R) -> Result<(), CoreError> + Send>;
75/// Push closure backing a persistent watcher sink.
76type WatcherPush<U> = Box<dyn FnMut(&U) -> Result<(), CoreError> + Send>;
77
78/// One-shot reply target for a command.
79///
80/// Consumed exactly once via [`Self::send`]; the closure performs the
81/// backend-specific delivery (socket: serialize and write to the peer fd;
82/// binder: send through a oneshot channel to the blocked transaction thread).
83pub struct ReplySink<R> {
84    send: ReplySend<R>,
85}
86
87impl<R> ReplySink<R> {
88    /// Wrap a delivery closure as a reply sink.
89    pub fn new(send: impl FnOnce(R) -> Result<(), CoreError> + Send + 'static) -> Self {
90        Self {
91            send: Box::new(send),
92        }
93    }
94
95    /// Deliver the reply, consuming the sink.
96    ///
97    /// ### Errors
98    /// Returns the backend's delivery error (e.g. the peer went away); the
99    /// caller may log it and drop the sink.
100    ///
101    /// ### Drop semantics
102    /// Dropping a [`ReplySink`] without calling [`Self::send`] loses the reply
103    /// (the closure is dropped). For a Binder backend the waiting transaction
104    /// thread observes the oneshot disconnect and errors out; the socket
105    /// backend simply never writes.
106    pub fn send(self, reply: R) -> Result<(), CoreError> {
107        (self.send)(reply)
108    }
109}
110
111impl<R> std::fmt::Debug for ReplySink<R> {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        f.debug_struct("ReplySink").finish_non_exhaustive()
114    }
115}
116
117/// Persistent push target for watch updates.
118///
119/// The channel logic holds one [`WatcherSink`] per registered watcher and
120/// calls [`Self::push`] to deliver a structured update value. A failed push
121/// (e.g. `EPIPE` on a gone socket) tells the channel logic the watcher is
122/// dead; the corresponding [`TransportEvent::WatcherClosed`] / `WatcherDied`
123/// still flows back through the ingestion path for cleanup.
124pub struct WatcherSink<U> {
125    push: WatcherPush<U>,
126}
127
128impl<U> WatcherSink<U> {
129    /// Wrap a push closure as a watcher sink.
130    pub fn new(push: impl FnMut(&U) -> Result<(), CoreError> + Send + 'static) -> Self {
131        Self {
132            push: Box::new(push),
133        }
134    }
135
136    /// Deliver one update value to the subscriber.
137    ///
138    /// ### Errors
139    /// Returns the backend's delivery error (e.g. the peer went away); the
140    /// caller should evict the watcher and stop pushing.
141    pub fn push(&mut self, update: &U) -> Result<(), CoreError> {
142        (self.push)(update)
143    }
144}
145
146impl<U> std::fmt::Debug for WatcherSink<U> {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        f.debug_struct("WatcherSink").finish_non_exhaustive()
149    }
150}
151
152/// A transport-agnostic command or watcher-lifecycle event.
153///
154/// Backends produce these; channel logic consumes them. `C` is the domain
155/// command type, `R` the domain reply type.
156#[derive(Debug)]
157pub enum TransportEvent<C, R> {
158    /// One command with the caller identity captured at delivery time and a
159    /// one-shot reply target.
160    Command {
161        /// The domain command.
162        cmd: C,
163        /// The calling uid captured synchronously by the backend when the
164        /// command arrived (`None` when the transport exposes no caller
165        /// identity, e.g. a socket backend with no auth policy).
166        calling_uid: Option<u32>,
167        /// The one-shot reply target, consumed exactly once.
168        reply: ReplySink<R>,
169    },
170    /// A watcher's transport went away (socket EOF / hangup, or a client
171    /// unwatch). The id was allocated by this loop.
172    WatcherClosed(WatcherId),
173    /// A watcher died remotely (binder death-recipient callback). The id was
174    /// allocated by this loop.
175    WatcherDied(WatcherId),
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use std::sync::Arc;
182    use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
183
184    #[test]
185    fn watcher_id_round_trips_value() {
186        let id = WatcherId::new(7);
187        assert_eq!(id.as_u64(), 7);
188        assert_eq!(WatcherId::new(7), id);
189        assert_ne!(WatcherId::new(8), id);
190    }
191
192    #[test]
193    fn reply_sink_delivers_exactly_once() {
194        let delivered = Arc::new(AtomicU32::new(0));
195        let captured = Arc::clone(&delivered);
196        let sink = ReplySink::new(move |reply: u32| {
197            captured.fetch_add(reply, Ordering::SeqCst);
198            Ok(())
199        });
200        sink.send(42).unwrap();
201        assert_eq!(delivered.load(Ordering::SeqCst), 42);
202    }
203
204    #[test]
205    fn reply_sink_propagates_backend_error() {
206        let sink = ReplySink::new(|_: u32| Err(CoreError::sys(libc::EPIPE, "reply")));
207        let err = sink.send(1).unwrap_err();
208        assert_eq!(err.raw_os_error(), Some(libc::EPIPE));
209    }
210
211    #[test]
212    fn watcher_sink_pushes_borrowed_updates() {
213        let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
214        let captured = Arc::clone(&seen);
215        let mut sink = WatcherSink::new(move |update: &u32| {
216            captured.lock().unwrap().push(*update);
217            Ok(())
218        });
219        sink.push(&1).unwrap();
220        sink.push(&2).unwrap();
221        assert_eq!(*seen.lock().unwrap(), vec![1, 2]);
222    }
223
224    #[test]
225    fn watcher_sink_propagates_push_error() {
226        let mut sink = WatcherSink::new(|_: &u32| Err(CoreError::sys(libc::EPIPE, "push")));
227        let err = sink.push(&1).unwrap_err();
228        assert_eq!(err.raw_os_error(), Some(libc::EPIPE));
229    }
230
231    #[derive(Debug)]
232    enum Cmd {
233        Ping,
234    }
235    #[derive(Debug)]
236    enum Reply {
237        Pong,
238    }
239
240    #[test]
241    fn transport_event_carries_command_and_identity() {
242        let answered = Arc::new(AtomicUsize::new(0));
243        let captured = Arc::clone(&answered);
244        let event = TransportEvent::Command {
245            cmd: Cmd::Ping,
246            calling_uid: Some(1000),
247            reply: ReplySink::new(move |r: Reply| {
248                assert!(matches!(r, Reply::Pong));
249                captured.fetch_add(1, Ordering::SeqCst);
250                Ok(())
251            }),
252        };
253        match event {
254            TransportEvent::Command {
255                cmd,
256                calling_uid,
257                reply,
258            } => {
259                assert!(matches!(cmd, Cmd::Ping));
260                assert_eq!(calling_uid, Some(1000));
261                reply.send(Reply::Pong).unwrap();
262            }
263            _ => panic!("expected command event"),
264        }
265        assert_eq!(answered.load(Ordering::SeqCst), 1);
266    }
267
268    #[test]
269    fn transport_event_carries_watcher_close_and_died() {
270        let id = WatcherId::new(3);
271        match TransportEvent::<Cmd, Reply>::WatcherClosed(id) {
272            TransportEvent::WatcherClosed(got) => assert_eq!(got, id),
273            _ => panic!("expected watcher closed"),
274        }
275        match TransportEvent::<Cmd, Reply>::WatcherDied(id) {
276            TransportEvent::WatcherDied(got) => assert_eq!(got, id),
277            _ => panic!("expected watcher died"),
278        }
279    }
280
281    #[test]
282    fn sinks_are_debug_but_opaque() {
283        let sink: ReplySink<u32> = ReplySink::new(|_| Ok(()));
284        let text = format!("{sink:?}");
285        assert!(text.starts_with("ReplySink"));
286
287        let watcher: WatcherSink<u32> = WatcherSink::new(|_| Ok(()));
288        let text = format!("{watcher:?}");
289        assert!(text.starts_with("WatcherSink"));
290    }
291}