Skip to main content

dspk_stream/transport/
server.rs

1//! The server side: accepts clients and serves a recording to each (header, signal, views).
2
3use std::net::SocketAddr;
4use std::path::Path;
5use std::sync::{Arc, Mutex};
6use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8use dspk_core::{RecordingInfo, RecordingSource, SampleFormat};
9use dspk_view::{Envelope, Pyramid};
10use tokio::task::JoinHandle;
11use tokio::time::{interval, MissedTickBehavior};
12
13use super::auth::{Authority, Peer, Permission};
14use super::files::{kind_of, DataRoot};
15use super::jobs::Jobs;
16use crate::error::{StreamError, StreamResult};
17use crate::protocol::{frame_from_envelope, header_from_info, read_message, view_from_request, wire, write_message, PROTOCOL_VERSION};
18use dspk_view::View;
19
20/// Seconds of signal per frame by default.
21pub const DEFAULT_FRAME_SEC: f64 = 0.02;
22/// Frames read ahead per client by default (bounds memory whatever the recording length).
23pub const DEFAULT_QUEUE_FRAMES: usize = 8;
24/// Largest message a client may send by default: its messages (hello, subscribe, view
25/// requests) are a few bytes; the length is checked before anything is allocated.
26pub const DEFAULT_MAX_REQUEST_BYTES: usize = 64 << 10;
27/// Clients connected at once by default; more are refused until one leaves.
28pub const DEFAULT_MAX_CONNECTIONS: usize = 32;
29/// Largest view answered: values (channels × columns, or × samples) in one envelope, so a request
30/// cannot make the server allocate without bound (32 MiB of f32, under a client's message limit).
31pub const MAX_VIEW_VALUES: u64 = 8 << 20;
32/// Most columns a view may ask for (a screen is a few thousand pixels wide).
33pub const MAX_VIEW_COLUMNS: u32 = 1 << 16;
34
35/// When frames are sent.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Pacing {
38    /// At the recording's rate, as an acquisition system would.
39    RealTime,
40    /// As fast as the client takes them (throughput tests, bulk transfer).
41    Unpaced,
42}
43
44/// How a recording is served.
45#[derive(Debug, Clone)]
46pub struct ServeOptions {
47    /// Seconds of signal per frame.
48    pub frame_sec: f64,
49    pub pacing: Pacing,
50    /// Start over at the end of the recording.
51    pub loop_playback: bool,
52    /// Frames read ahead per client.
53    pub queue_frames: usize,
54    /// Largest message accepted from a client (default [`DEFAULT_MAX_REQUEST_BYTES`]).
55    pub max_message_bytes: usize,
56}
57
58impl Default for ServeOptions {
59    fn default() -> Self {
60        Self { frame_sec: DEFAULT_FRAME_SEC, pacing: Pacing::RealTime, loop_playback: false, queue_frames: DEFAULT_QUEUE_FRAMES, max_message_bytes: DEFAULT_MAX_REQUEST_BYTES }
61    }
62}
63
64/// A QUIC endpoint accepting clients.
65pub struct Server {
66    endpoint: quinn::Endpoint,
67    max_connections: usize,
68    authority: Option<Arc<Authority>>,
69}
70
71/// Closing code for a client whose certificate is not enrolled or is revoked.
72pub const REFUSED_CLIENT: u32 = 1;
73
74impl Server {
75    /// Listens on `addr` (UDP), at most [`DEFAULT_MAX_CONNECTIONS`] clients at once.
76    pub fn bind(addr: SocketAddr, config: quinn::ServerConfig) -> StreamResult<Self> {
77        Ok(Self { endpoint: quinn::Endpoint::server(config, addr)?, max_connections: DEFAULT_MAX_CONNECTIONS, authority: None })
78    }
79
80    /// Checks every client's certificate against `authority`'s registry after the handshake (use
81    /// with a configuration from [`super::tls::server_config_with_clients`]): enrolled and not
82    /// revoked, or the connection is closed.
83    pub fn with_authority(mut self, authority: Arc<Authority>) -> Self {
84        self.authority = Some(authority);
85        self
86    }
87
88    /// At most `n` clients at once (at least one).
89    pub fn with_max_connections(mut self, n: usize) -> Self {
90        self.max_connections = n.max(1);
91        self
92    }
93
94    pub fn local_addr(&self) -> StreamResult<SocketAddr> {
95        Ok(self.endpoint.local_addr()?)
96    }
97
98    /// The next client whose handshake succeeds and who is allowed in, and who it is; `None`
99    /// once the endpoint is closed. Before any handshake work, a client must prove it owns its
100    /// address (QUIC Retry: a spoofed source address gets one small packet, never the server's
101    /// answers), and clients beyond the connection limit are refused. With an authority, a
102    /// client presenting a certificate must be enrolled and not revoked; one presenting none is
103    /// anonymous (only possible when the TLS configuration allows it).
104    pub async fn accept(&self) -> Option<(quinn::Connection, Peer)> {
105        loop {
106            let incoming = self.endpoint.accept().await?;
107            if !incoming.remote_address_validated() && incoming.may_retry() {
108                if let Err(e) = incoming.retry() {
109                    tracing::debug!("QUIC retry failed: {e}");
110                }
111                continue;
112            }
113            if self.endpoint.open_connections() >= self.max_connections {
114                tracing::warn!("refused {}: {} clients connected (the limit)", incoming.remote_address(), self.max_connections);
115                incoming.refuse();
116                continue;
117            }
118            let connection = match incoming.await {
119                Ok(connection) => connection,
120                Err(e) => {
121                    tracing::warn!("QUIC handshake failed: {e}");
122                    continue;
123                }
124            };
125            match self.peer(&connection) {
126                Some(peer) => return Some((connection, peer)),
127                None => {
128                    tracing::warn!("refused {}: its certificate is not enrolled or is revoked", connection.remote_address());
129                    connection.close(quinn::VarInt::from_u32(REFUSED_CLIENT), b"client not enrolled or revoked");
130                }
131            }
132        }
133    }
134
135    /// Who `connection`'s client is: anonymous without an authority or without a certificate,
136    /// else its enrolled record.
137    fn peer(&self, connection: &quinn::Connection) -> Option<Peer> {
138        let Some(authority) = &self.authority else { return Some(Peer::anonymous()) };
139        let certificates = connection.peer_identity().and_then(|id| id.downcast::<Vec<rustls::pki_types::CertificateDer<'static>>>().ok());
140        match certificates.as_deref().and_then(|c| c.first()) {
141            Some(certificate) => authority.authorize(certificate),
142            None => Some(Peer::anonymous()),
143        }
144    }
145
146    /// Stops accepting and closes every connection.
147    pub fn close(&self) {
148        self.endpoint.close(quinn::VarInt::from_u32(0), b"server closed");
149    }
150}
151
152/// What the signal stream carries: stored words when the source can read them, else scaled
153/// `f32` (and the header then says `f32`, gain 1, offset 0).
154fn streamed_info(source: &dyn RecordingSource) -> (RecordingInfo, bool) {
155    let info = source.info().clone();
156    let all: Vec<usize> = (0..info.channel_count()).collect();
157    // One sample of every channel tells whether stored reads are supported
158    let mut probe = vec![0u8; all.len() * info.format.bytes()];
159    let stored = info.samples == 0 || source.read_stored(&all, 0..1, &mut probe).is_ok();
160    if stored {
161        return (info, true);
162    }
163    let mut scaled = info;
164    scaled.format = SampleFormat::F32;
165    for c in &mut scaled.channels {
166        (c.gain, c.offset) = (1.0, 0.0);
167    }
168    (scaled, false)
169}
170
171/// A recording a session serves: its source, pyramid (views), and what the signal carries.
172pub struct OpenRecording {
173    source: Arc<dyn RecordingSource>,
174    pyramid: Option<Arc<Pyramid>>,
175    info: RecordingInfo,
176    stored: bool,
177}
178
179impl OpenRecording {
180    pub fn new(source: Arc<dyn RecordingSource>, pyramid: Option<Arc<Pyramid>>) -> Self {
181        let (info, stored) = streamed_info(source.as_ref());
182        Self { source, pyramid, info, stored }
183    }
184
185    /// What the signal stream carries (the header sent to clients).
186    pub fn info(&self) -> &RecordingInfo {
187        &self.info
188    }
189}
190
191/// Opens the recordings clients ask for, by path under the data root. The formats are the
192/// caller's (dspk-stream knows none): the command line opens through dspk-synapse.
193pub trait RecordingOpener: Send + Sync {
194    /// Opens `path` (`source`: one of the file's sources, `None` = its default).
195    fn open(&self, path: &Path, source: Option<&str>) -> StreamResult<OpenRecording>;
196}
197
198/// What a server offers every client.
199#[derive(Clone)]
200pub struct Device {
201    /// The server's name (its host) and version, shown to clients.
202    pub name: String,
203    pub version: String,
204    /// The data root and how recordings under it are opened; `None`: no files.
205    pub files: Option<(DataRoot, Arc<dyn RecordingOpener>)>,
206    /// The recording every client starts with, if any.
207    pub recording: Option<Arc<OpenRecording>>,
208    /// The jobs it runs for clients (sorts); `None`: none.
209    pub jobs: Option<Arc<Jobs>>,
210    /// The compute runtime its jobs run on, as clients are told ("" = none or unknown).
211    pub runtime: String,
212    pub options: ServeOptions,
213}
214
215/// The recording a session has open (the initial one, or the last one it opened).
216type Current = Arc<Mutex<Option<Arc<OpenRecording>>>>;
217
218fn current_recording(current: &Current) -> Option<Arc<OpenRecording>> {
219    current.lock().unwrap_or_else(|e| e.into_inner()).clone()
220}
221
222/// Serves `source` to one client (the protocol's single-recording form: no files, anonymous).
223pub async fn serve_recording(connection: quinn::Connection, source: Arc<dyn RecordingSource>, pyramid: Option<Arc<Pyramid>>, options: ServeOptions) -> StreamResult<()> {
224    let device = Device { name: "dspk".into(), version: String::new(), files: None, recording: Some(Arc::new(OpenRecording::new(source, pyramid))), jobs: None, runtime: String::new(), options };
225    serve_session(connection, Peer::anonymous(), Arc::new(device)).await
226}
227
228/// Serves one client until it closes the control stream or disconnects: answers its hello with
229/// the server's info, streams the signal of its open recording from where it subscribes, and
230/// answers every request (views, files, opening a recording) on its own stream.
231pub async fn serve_session(connection: quinn::Connection, peer: Peer, device: Arc<Device>) -> StreamResult<()> {
232    let options = device.options.clone();
233    let (mut control_send, mut control_recv) = connection.accept_bi().await?;
234    let hello: wire::ClientHello = read_message(&mut control_recv, options.max_message_bytes).await?.ok_or_else(|| StreamError::protocol("no hello"))?;
235    let info = wire::ServerInfo {
236        protocol_version: PROTOCOL_VERSION,
237        server_name: device.name.clone(),
238        server_version: device.version.clone(),
239        peer_name: peer.name.clone(),
240        permission: wire_permission(peer.permission) as i32,
241        state: device.jobs.as_ref().map_or(wire::DeviceState::Stopped, |j| j.state()) as i32,
242        has_files: device.files.is_some(),
243        recording: device.recording.as_ref().map(|r| header_from_info(r.info())),
244        runtime: device.runtime.clone(),
245    };
246    write_message(&mut control_send, &info).await?;
247    if hello.protocol_version != PROTOCOL_VERSION {
248        return Err(StreamError::protocol(format!("client speaks protocol {}, this server {PROTOCOL_VERSION}", hello.protocol_version)));
249    }
250
251    let current: Current = Arc::new(Mutex::new(device.recording.clone()));
252    let requests = tokio::spawn(answer_requests(connection.clone(), device.clone(), current.clone(), Arc::new(peer)));
253    let mut signal: Option<JoinHandle<StreamResult<()>>> = None;
254    let result = loop {
255        match read_message::<wire::Subscribe>(&mut control_recv, options.max_message_bytes).await {
256            Ok(Some(subscribe)) => {
257                let Some(recording) = current_recording(&current) else { break Err(StreamError::protocol("subscribe before any recording is open")) };
258                if let Some(old) = signal.take() {
259                    old.abort();
260                }
261                let task = stream_signal(connection.clone(), recording, subscribe.first_sample, options.clone());
262                signal = Some(tokio::spawn(task));
263            }
264            Ok(None) => break Ok(()),
265            Err(e) if e.is_closed_by_peer() => break Ok(()),
266            Err(e) => break Err(e),
267        }
268    };
269    if let Some(task) = signal {
270        task.abort();
271    }
272    requests.abort();
273    result
274}
275
276fn wire_permission(p: Permission) -> wire::Permission {
277    match p {
278        Permission::Read => wire::Permission::Read,
279        Permission::Control => wire::Permission::Control,
280    }
281}
282
283/// Answers each request stream the client opens with one response.
284async fn answer_requests(connection: quinn::Connection, device: Arc<Device>, current: Current, peer: Arc<Peer>) {
285    let max = device.options.max_message_bytes;
286    while let Ok((mut send, mut recv)) = connection.accept_bi().await {
287        let (device, current, peer) = (device.clone(), current.clone(), peer.clone());
288        tokio::spawn(async move {
289            let answer = async {
290                let request: wire::Request = read_message(&mut recv, max).await?.ok_or_else(|| StreamError::protocol("empty request stream"))?;
291                if let Some(wire::request::Kind::Fetch(fetch)) = &request.kind {
292                    return send_file(&mut send, &device, &fetch.path).await;
293                }
294                let kind = match answer(request, &device, &current, &peer).await {
295                    Ok(kind) => kind,
296                    Err(e) => wire::response::Kind::Error(e.reason()),
297                };
298                write_message(&mut send, &wire::Response { kind: Some(kind) }).await?;
299                send.finish().map_err(StreamError::protocol)?;
300                StreamResult::Ok(())
301            };
302            if let Err(e) = answer.await {
303                tracing::debug!("request stream closed: {e}");
304            }
305        });
306    }
307}
308
309/// The answer to one request.
310async fn answer(request: wire::Request, device: &Device, current: &Current, peer: &Peer) -> StreamResult<wire::response::Kind> {
311    use wire::request::Kind as Req;
312    use wire::response::Kind as Resp;
313    match request.kind.ok_or_else(|| StreamError::protocol("empty request"))? {
314        Req::View(view) => {
315            let recording = current_recording(current).ok_or_else(|| StreamError::protocol("no recording is open"))?;
316            let view = checked_view(&view, recording.info())?;
317            let envelope = tokio::task::spawn_blocking(move || -> StreamResult<Envelope> { Ok(view.read(recording.source.as_ref(), recording.pyramid.as_deref())?) })
318                .await
319                .map_err(StreamError::protocol)?;
320            Ok(Resp::Envelope(frame_from_envelope(envelope)))
321        }
322        Req::ListFiles(list) => {
323            let (root, _) = device.files.as_ref().ok_or_else(|| StreamError::protocol("this server serves no files"))?;
324            let root = root.clone();
325            let files = tokio::task::spawn_blocking(move || root.list(&list.dir)).await.map_err(StreamError::protocol)??;
326            Ok(Resp::Files(files))
327        }
328        Req::Open(open) => {
329            let (root, opener) = device.files.clone().ok_or_else(|| StreamError::protocol("this server serves no files"))?;
330            let recording = tokio::task::spawn_blocking(move || -> StreamResult<OpenRecording> {
331                let path = root.resolve(&open.path)?;
332                if kind_of(&path, path.is_dir()) != wire::FileKind::Recording {
333                    return Err(StreamError::Protocol(format!("{:?} is not a recording", open.path)));
334                }
335                opener.open(&path, Some(open.source.as_str()).filter(|s| !s.is_empty()))
336            })
337            .await
338            .map_err(StreamError::protocol)??;
339            let header = header_from_info(recording.info());
340            *current.lock().unwrap_or_else(|e| e.into_inner()) = Some(Arc::new(recording));
341            Ok(Resp::Header(header))
342        }
343        Req::Configure(configure) => {
344            let jobs = jobs_of(device)?;
345            let peer = peer.clone();
346            Ok(Resp::Status(tokio::task::spawn_blocking(move || jobs.configure(&peer, configure)).await.map_err(StreamError::protocol)??))
347        }
348        Req::Start(_) => Ok(Resp::Status(jobs_of(device)?.start(peer)?)),
349        Req::Stop(_) => Ok(Resp::Status(jobs_of(device)?.stop(peer)?)),
350        Req::Status(_) => Ok(Resp::Status(jobs_of(device)?.status())),
351        Req::Logs(logs) => Ok(Resp::Logs(wire::LogLines { lines: jobs_of(device)?.logs(logs.lines as usize) })),
352        Req::Fetch(_) => Err(StreamError::protocol("fetch is answered by send_file")),
353    }
354}
355
356/// Bytes per FileChunk.
357pub const FILE_CHUNK_BYTES: usize = 1 << 20;
358
359/// Answers a Fetch: the file's header, then its bytes in chunks; a refusal as an error response.
360async fn send_file(send: &mut quinn::SendStream, device: &Device, path: &str) -> StreamResult<()> {
361    use tokio::io::AsyncReadExt as _;
362    let opened = async {
363        let (root, _) = device.files.as_ref().ok_or_else(|| StreamError::Server("this server serves no files".into()))?;
364        let full = root.resolve(path)?;
365        if !full.is_file() {
366            return Err(StreamError::Server(format!("{path:?} is not a file (fetch a directory file by file)")));
367        }
368        let file = tokio::fs::File::open(&full).await?;
369        let size = file.metadata().await?.len();
370        Ok((file, size, root.relative(&full)))
371    };
372    let (mut file, size, relative) = match opened.await {
373        Ok(o) => o,
374        Err(e) => {
375            write_message(send, &wire::Response { kind: Some(wire::response::Kind::Error(e.reason())) }).await?;
376            return send.finish().map_err(StreamError::protocol);
377        }
378    };
379    write_message(send, &wire::Response { kind: Some(wire::response::Kind::File(wire::FileHeader { path: relative, size })) }).await?;
380    let mut buf = vec![0u8; FILE_CHUNK_BYTES];
381    loop {
382        let n = file.read(&mut buf).await?;
383        if n == 0 {
384            break;
385        }
386        write_message(send, &wire::FileChunk { data: buf[..n].to_vec() }).await?;
387    }
388    send.finish().map_err(StreamError::protocol)
389}
390
391fn jobs_of(device: &Device) -> StreamResult<Arc<Jobs>> {
392    device.jobs.clone().ok_or_else(|| StreamError::Server("this server runs no jobs".into()))
393}
394
395/// The view a client asks for, refused when it is outside the recording or larger than the
396/// server answers: channels in range and each once, at most [`MAX_VIEW_COLUMNS`] columns and
397/// [`MAX_VIEW_VALUES`] values (channels × the columns, or the samples when fewer).
398pub fn checked_view(request: &wire::ViewRequest, info: &RecordingInfo) -> StreamResult<View> {
399    let view = view_from_request(request);
400    view.validate(info.channel_count(), info.samples).map_err(|e| StreamError::protocol(e.to_string()))?;
401    let mut seen = vec![false; info.channel_count()];
402    for &c in &view.channels {
403        if std::mem::replace(&mut seen[c], true) {
404            return Err(StreamError::protocol(format!("channel {c} asked for twice")));
405        }
406    }
407    if request.width > MAX_VIEW_COLUMNS {
408        return Err(StreamError::protocol(format!("{} columns asked for: at most {MAX_VIEW_COLUMNS}", request.width)));
409    }
410    let values = view.channels.len() as u64 * (view.end - view.start).min(u64::from(request.width));
411    if values > MAX_VIEW_VALUES {
412        return Err(StreamError::protocol(format!("a view of {values} values: at most {MAX_VIEW_VALUES} (fewer channels or columns)")));
413    }
414    Ok(view)
415}
416
417/// Streams frames from `first_sample` on a new unidirectional stream. A reader thread fills a
418/// bounded queue, so the recording is read ahead of the network but never held whole; the thread
419/// ends by itself once the queue is dropped.
420async fn stream_signal(connection: quinn::Connection, recording: Arc<OpenRecording>, first_sample: u64, options: ServeOptions) -> StreamResult<()> {
421    let (source, info, stored) = (recording.source.clone(), recording.info.clone(), recording.stored);
422    let total = info.samples;
423    let frame = ((options.frame_sec * info.sample_rate_hz()).round() as u64).max(1);
424    let channels: Vec<usize> = (0..info.channel_count()).collect();
425    let bytes_per_value = info.format.bytes();
426    let (tx, mut rx) = tokio::sync::mpsc::channel::<StreamResult<(u64, u32, Vec<u8>)>>(options.queue_frames.max(1));
427    let loop_playback = options.loop_playback;
428    std::thread::spawn(move || {
429        let mut start = first_sample.min(total);
430        loop {
431            if start >= total {
432                if !loop_playback || total == 0 {
433                    return;
434                }
435                start = 0;
436            }
437            let end = (start + frame).min(total);
438            let n = (end - start) as usize;
439            let mut bytes = vec![0u8; channels.len() * n * bytes_per_value];
440            let read = if stored {
441                source.read_stored(&channels, start..end, &mut bytes)
442            } else {
443                let mut values = vec![0.0f32; channels.len() * n];
444                source.read(&channels, start..end, &mut values).map(|()| {
445                    for (dst, v) in bytes.chunks_exact_mut(bytes_per_value).zip(values) {
446                        dst.copy_from_slice(&v.to_le_bytes());
447                    }
448                })
449            };
450            let message = read.map(|()| (start, n as u32, bytes)).map_err(StreamError::from);
451            let failed = message.is_err();
452            if tx.blocking_send(message).is_err() || failed {
453                return;
454            }
455            start = end;
456        }
457    });
458
459    let mut send = connection.open_uni().await?;
460    let mut ticker = interval(Duration::from_secs_f64(frame as f64 / info.sample_rate_hz()));
461    ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
462    let mut sequence = 0u64;
463    async {
464        while let Some(chunk) = rx.recv().await {
465            let (first_sample, samples, data) = chunk?;
466            if options.pacing == Pacing::RealTime {
467                ticker.tick().await;
468            }
469            let sent_unix_ns = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_nanos() as u64);
470            write_message(&mut send, &wire::SignalFrame { sequence, first_sample, samples, data, sent_unix_ns }).await?;
471            sequence += 1;
472        }
473        send.finish().map_err(StreamError::protocol)?;
474        StreamResult::Ok(())
475    }
476    .await
477}