Skip to main content

dspk_stream/transport/
client.rs

1//! The client side: a session with one server (who it is, its files, the recording this client
2//! opened, the signal, views).
3
4use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
5
6use dspk_core::RecordingInfo;
7use dspk_view::{Envelope, View};
8
9use crate::error::{StreamError, StreamResult};
10use crate::protocol::{
11    decode_signal, envelope_from_frame, info_from_header, read_message, request_from_view, wire, write_message, DEFAULT_MAX_MESSAGE_BYTES, PROTOCOL_VERSION,
12};
13
14/// A connected session: the server's info, and the recording this client has open.
15pub struct Session {
16    /// Kept for the connection's lifetime (the endpoint drives it).
17    _endpoint: quinn::Endpoint,
18    connection: quinn::Connection,
19    control: quinn::SendStream,
20    /// Kept open: dropping it would ask the server to stop the control stream.
21    _control_recv: quinn::RecvStream,
22    server: wire::ServerInfo,
23    recording: Option<RecordingInfo>,
24    max_message_bytes: usize,
25}
26
27impl Session {
28    /// Connects to `addr` (certificate checked against `server_name` per `config`) and reads the
29    /// server's info. The local endpoint uses the address family of `addr`.
30    pub async fn connect(addr: SocketAddr, server_name: &str, config: quinn::ClientConfig) -> StreamResult<Self> {
31        let local: SocketAddr = if addr.is_ipv6() { (Ipv6Addr::UNSPECIFIED, 0).into() } else { (Ipv4Addr::UNSPECIFIED, 0).into() };
32        let mut endpoint = quinn::Endpoint::client(local)?;
33        endpoint.set_default_client_config(config);
34        let connection = endpoint.connect(addr, server_name)?.await?;
35        let (mut control, mut control_recv) = connection.open_bi().await?;
36        write_message(&mut control, &wire::ClientHello { protocol_version: PROTOCOL_VERSION }).await?;
37        let server: wire::ServerInfo = read_message(&mut control_recv, DEFAULT_MAX_MESSAGE_BYTES).await?.ok_or_else(|| StreamError::protocol("no server info"))?;
38        if server.protocol_version != PROTOCOL_VERSION {
39            return Err(StreamError::protocol(format!("server speaks protocol {}, this client {PROTOCOL_VERSION}", server.protocol_version)));
40        }
41        let recording = server.recording.as_ref().map(info_from_header).transpose()?;
42        Ok(Self { _endpoint: endpoint, connection, control, _control_recv: control_recv, server, recording, max_message_bytes: DEFAULT_MAX_MESSAGE_BYTES })
43    }
44
45    /// Accepts messages up to `bytes` long (default [`DEFAULT_MAX_MESSAGE_BYTES`]).
46    pub fn with_max_message_bytes(mut self, bytes: usize) -> Self {
47        self.max_message_bytes = bytes;
48        self
49    }
50
51    /// Who the server is, who this client is to it, and what it may do.
52    pub fn server(&self) -> &wire::ServerInfo {
53        &self.server
54    }
55
56    /// The recording this client has open (channels, units, exact rate, stored format): the
57    /// server's initial one, or the last [`Self::open`]ed.
58    pub fn recording(&self) -> Option<&RecordingInfo> {
59        self.recording.as_ref()
60    }
61
62    /// Starts (or restarts) the signal of the open recording at `first_sample`.
63    pub async fn subscribe(&mut self, first_sample: u64) -> StreamResult<SignalReceiver> {
64        let info = self.recording.clone().ok_or_else(|| StreamError::protocol("no recording is open: open one first"))?;
65        write_message(&mut self.control, &wire::Subscribe { first_sample }).await?;
66        let recv = self.connection.accept_uni().await?;
67        Ok(SignalReceiver { recv, info, max_message_bytes: self.max_message_bytes })
68    }
69
70    /// Sends `kind` on a stream of its own (never behind signal data) and reads the answer; a
71    /// refusal or failure on the server is an error here.
72    async fn request(&self, kind: wire::request::Kind) -> StreamResult<wire::response::Kind> {
73        let (mut send, mut recv) = self.connection.open_bi().await?;
74        write_message(&mut send, &wire::Request { kind: Some(kind) }).await?;
75        send.finish().map_err(StreamError::protocol)?;
76        let response: wire::Response = read_message(&mut recv, self.max_message_bytes).await?.ok_or_else(|| StreamError::protocol("no response"))?;
77        match response.kind {
78            Some(wire::response::Kind::Error(message)) => Err(StreamError::Server(message)),
79            Some(kind) => Ok(kind),
80            None => Err(StreamError::protocol("empty response")),
81        }
82    }
83
84    /// The server's answer to `view` of the open recording.
85    pub async fn view(&self, view: &View) -> StreamResult<Envelope> {
86        match self.request(wire::request::Kind::View(request_from_view(view)?)).await? {
87            wire::response::Kind::Envelope(frame) => envelope_from_frame(frame),
88            _ => Err(StreamError::protocol("unexpected answer to a view")),
89        }
90    }
91
92    /// The entries of directory `dir` under the server's data root ("" = the root).
93    pub async fn list_files(&self, dir: &str) -> StreamResult<wire::FileList> {
94        match self.request(wire::request::Kind::ListFiles(wire::ListFiles { dir: dir.into() })).await? {
95            wire::response::Kind::Files(list) => Ok(list),
96            _ => Err(StreamError::protocol("unexpected answer to a listing")),
97        }
98    }
99
100    async fn status_request(&self, kind: wire::request::Kind) -> StreamResult<wire::Status> {
101        match self.request(kind).await? {
102            wire::response::Kind::Status(status) => Ok(status),
103            _ => Err(StreamError::protocol("unexpected answer to a job request")),
104        }
105    }
106
107    /// Sets the server's job (control permission; only while nothing runs).
108    pub async fn configure(&self, job: wire::Configure) -> StreamResult<wire::Status> {
109        self.status_request(wire::request::Kind::Configure(job)).await
110    }
111
112    /// Starts the configured job (control permission).
113    pub async fn start(&self) -> StreamResult<wire::Status> {
114        self.status_request(wire::request::Kind::Start(wire::Start {})).await
115    }
116
117    /// Asks the running job to stop (control permission).
118    pub async fn stop(&self) -> StreamResult<wire::Status> {
119        self.status_request(wire::request::Kind::Stop(wire::Stop {})).await
120    }
121
122    /// The server's state and its job's progress.
123    pub async fn status(&self) -> StreamResult<wire::Status> {
124        self.status_request(wire::request::Kind::Status(wire::GetStatus {})).await
125    }
126
127    /// The job's last `lines` log lines (0 = all the server keeps).
128    pub async fn logs(&self, lines: u32) -> StreamResult<Vec<String>> {
129        match self.request(wire::request::Kind::Logs(wire::GetLogs { lines })).await? {
130            wire::response::Kind::Logs(logs) => Ok(logs.lines),
131            _ => Err(StreamError::protocol("unexpected answer to logs")),
132        }
133    }
134
135    /// Fetches the file `path` under the server's data root into `dest` (a new file, never
136    /// overwritten); returns its size. A directory is fetched with [`Self::fetch`].
137    pub async fn fetch_file(&self, path: &str, dest: &std::path::Path) -> StreamResult<u64> {
138        use tokio::io::AsyncWriteExt as _;
139        let (mut send, mut recv) = self.connection.open_bi().await?;
140        write_message(&mut send, &wire::Request { kind: Some(wire::request::Kind::Fetch(wire::Fetch { path: path.into() })) }).await?;
141        send.finish().map_err(StreamError::protocol)?;
142        let response: wire::Response = read_message(&mut recv, self.max_message_bytes).await?.ok_or_else(|| StreamError::protocol("no response"))?;
143        let header = match response.kind {
144            Some(wire::response::Kind::File(header)) => header,
145            Some(wire::response::Kind::Error(message)) => return Err(StreamError::Server(message)),
146            _ => return Err(StreamError::protocol("unexpected answer to fetch")),
147        };
148        let mut out = tokio::fs::OpenOptions::new().write(true).create_new(true).open(dest).await?;
149        let mut written = 0u64;
150        while let Some(chunk) = read_message::<wire::FileChunk>(&mut recv, self.max_message_bytes).await? {
151            written += chunk.data.len() as u64;
152            if written > header.size {
153                return Err(StreamError::protocol("the server sent more than the file's size"));
154            }
155            out.write_all(&chunk.data).await?;
156        }
157        out.flush().await?;
158        if written != header.size {
159            return Err(StreamError::Truncated);
160        }
161        Ok(written)
162    }
163
164    /// Fetches `path` (a file, or a directory such as a .sorting.zarr, file by file) into the new
165    /// local path `dest`; returns files and bytes. Names from the server are checked: plain, not
166    /// hidden, no separators, so nothing lands outside `dest`.
167    pub async fn fetch(&self, path: &str, dest: &std::path::Path) -> StreamResult<(usize, u64)> {
168        let parent = path.rsplit_once('/').map_or("", |(dir, _)| dir);
169        let name = path.rsplit('/').next().unwrap_or(path);
170        let listing = self.list_files(parent).await?;
171        let entry = listing.entries.iter().find(|e| e.name == name).ok_or_else(|| StreamError::Server(format!("{path:?} not found")))?;
172        if !entry.directory {
173            return Ok((1, self.fetch_file(path, dest).await?));
174        }
175        if dest.symlink_metadata().is_ok() {
176            return Err(StreamError::Io(std::io::Error::new(std::io::ErrorKind::AlreadyExists, format!("{} exists (never overwritten)", dest.display()))));
177        }
178        tokio::fs::create_dir(dest).await?;
179        let (mut files, mut bytes) = (0usize, 0u64);
180        // Depth-first over the directory, file by file
181        let mut stack = vec![(path.to_string(), dest.to_path_buf())];
182        while let Some((remote, local)) = stack.pop() {
183            for e in self.list_files(&remote).await?.entries {
184                let plain = !e.name.is_empty() && !e.name.starts_with('.') && !e.name.contains(['/', '\\']) && e.name != "..";
185                if !plain {
186                    return Err(StreamError::protocol(format!("the server listed {:?}: not a plain name", e.name)));
187                }
188                let (r, l) = (format!("{remote}/{}", e.name), local.join(&e.name));
189                if e.directory {
190                    tokio::fs::create_dir(&l).await?;
191                    stack.push((r, l));
192                } else {
193                    bytes += self.fetch_file(&r, &l).await?;
194                    files += 1;
195                }
196            }
197        }
198        Ok((files, bytes))
199    }
200
201    /// Opens the recording at `path` under the server's data root (`source`: one of the file's
202    /// sources, `None` = its default); views and the signal are then of it.
203    pub async fn open(&mut self, path: &str, source: Option<&str>) -> StreamResult<&RecordingInfo> {
204        let open = wire::Open { path: path.into(), source: source.unwrap_or_default().into() };
205        match self.request(wire::request::Kind::Open(open)).await? {
206            wire::response::Kind::Header(header) => {
207                self.recording = Some(info_from_header(&header)?);
208                Ok(self.recording.as_ref().expect("just set"))
209            }
210            _ => Err(StreamError::protocol("unexpected answer to open")),
211        }
212    }
213
214    /// Ends the session (the server stops streaming).
215    pub async fn close(mut self) {
216        let _ = self.control.finish();
217        self.connection.close(quinn::VarInt::from_u32(0), b"client closed");
218        self._endpoint.wait_idle().await;
219    }
220}
221
222/// Frames of the signal stream.
223pub struct SignalReceiver {
224    recv: quinn::RecvStream,
225    info: RecordingInfo,
226    max_message_bytes: usize,
227}
228
229impl SignalReceiver {
230    /// The next frame as sent (stored words); `None` at the end of the recording.
231    pub async fn next_frame(&mut self) -> StreamResult<Option<wire::SignalFrame>> {
232        read_message(&mut self.recv, self.max_message_bytes).await
233    }
234
235    /// The next frame's first sample and scaled values (`channels × samples`, channel-major).
236    pub async fn next_values(&mut self) -> StreamResult<Option<(u64, Vec<f32>)>> {
237        match self.next_frame().await? {
238            Some(frame) => Ok(Some((frame.first_sample, decode_signal(&frame, &self.info)?))),
239            None => Ok(None),
240        }
241    }
242}