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}