navcore_signalk_client/stream.rs
1//! Keeping the data stream open.
2//!
3//! A blocking connection on its own thread rather than an asynchronous
4//! one. The clients this feeds are user interfaces with main loops of
5//! their own, and asking each of them to host an async runtime for one
6//! socket is a poor trade. What comes out is a plain
7//! channel of events, which any main loop can drain.
8//!
9//! The reconnection is the point of the whole file. A wireless link on a
10//! boat drops; a client that needed restarting afterwards would be
11//! useless. Nothing here reports failure upwards as fatal -- it reports
12//! that the link is down, keeps trying, and lets the consumer decide what
13//! to show meanwhile. That decision has already been made elsewhere: the
14//! fix goes stale and says so.
15//!
16//! # Being told when to look, without being told how
17//!
18//! `wake`, passed to [`Connection::open`], is called from this thread the
19//! moment [`Connection::drain`] has something worth calling for. It is
20//! deliberately the only thing this crate knows about whichever main loop
21//! is on the other end -- a plain `Fn() + Send`, not a toolkit-specific
22//! idle source or anything else naming a toolkit, so that a client on
23//! any platform, and `sk-probe` on a bare terminal, all satisfy it the
24//! same way each already satisfies its own main loop.
25//!
26//! It is *told*, not *handed the data*: [`Event`]s still only ever leave
27//! through [`Connection::drain`], on the consumer's own timing. A callback
28//! invoked once per message would turn a burst of deltas after a reconnect
29//! into a burst of redraws, which is the one thing polling a receiver on a
30//! loop protected against by accident; `wake` protects against it on
31//! purpose, firing once for a burst and staying quiet until the consumer
32//! has drained and something new arrives after that.
33
34use std::io;
35use std::sync::atomic::{AtomicBool, Ordering};
36use std::sync::mpsc::{Receiver, Sender, channel};
37use std::sync::Arc;
38use std::thread;
39use std::time::Duration;
40
41use signalk::Delta;
42use tungstenite::client::IntoClientRequest;
43use tungstenite::http::HeaderValue;
44
45use crate::{ClientError, Trust};
46
47/// How long to wait before trying again, and the ceiling it grows to.
48///
49/// Starts short because most drops are a moment of interference; grows so
50/// that a server which is genuinely off does not get hammered all night.
51const FIRST_RETRY: Duration = Duration::from_secs(1);
52const LONGEST_RETRY: Duration = Duration::from_secs(30);
53
54/// How long [`read_until_broken`]'s own read blocks for before checking
55/// [`Connection::publish`]'s own outgoing queue and trying again.
56///
57/// Short enough that a message [`Connection::publish`] just queued reaches
58/// the wire promptly; long enough that this is not a busy-loop -- the same
59/// order of magnitude [`crate::access`]'s own polling already uses for "a
60/// human is somewhere in this loop, not a machine."
61const READ_POLL_INTERVAL: Duration = Duration::from_millis(250);
62
63/// What the connection reports.
64#[derive(Debug, Clone, PartialEq)]
65pub enum Event {
66 /// The stream is open. Carries the vessel identifier the server states
67 /// in its greeting, which is what own-ship deltas arrive under -- in
68 /// full, not as "vessels.self".
69 Connected {
70 /// The identifier from the greeting, if it had one.
71 self_id: Option<String>,
72 },
73 /// One message, already read into this workspace's units.
74 Delta(Delta),
75 /// The link is down. The consumer keeps whatever it last had, and lets
76 /// it go stale on screen.
77 Disconnected {
78 /// What went wrong, for the log rather than for the screen.
79 reason: String,
80 },
81 /// The server refused the handshake with `401 Unauthorized` -- the
82 /// token this connection was opened with is no longer accepted,
83 /// whether because it expired or because the administrator revoked
84 /// the device (the two look identical from here: nothing in a 401
85 /// itself says which). Told apart from an ordinary
86 /// [`Event::Disconnected`] because the right response is different in
87 /// kind, not just in wording: discard the token and ask again, rather
88 /// than retry the same one.
89 Unauthorized,
90}
91
92/// A running connection.
93///
94/// Dropping it asks the thread to stop after its current wait.
95#[derive(Debug)]
96pub struct Connection {
97 events: Receiver<Event>,
98 /// Shared with the background thread, which sets it -- see [`Sink`] --
99 /// and cleared here, in [`Connection::drain`], on the consumer's own
100 /// schedule rather than the thread's.
101 pending: Arc<AtomicBool>,
102 stop: Arc<AtomicBool>,
103 /// Messages queued for [`read_until_broken`]'s own thread to write to
104 /// the socket -- see [`Connection::publish`].
105 outgoing: Sender<String>,
106}
107
108impl Connection {
109 /// Opens the stream and keeps it open, reconnecting for as long as the
110 /// connection lives.
111 ///
112 /// `token` is optional, but a server with security on and `allowReadonly`
113 /// off will open this stream, greet the client, and then send it nothing
114 /// -- see [`crate::access`]. Passing `None` is therefore a choice to be
115 /// made knowingly, not a default to fall back on.
116 ///
117 /// `trust` decides which server may answer a `wss://` URL. It is asked
118 /// for on every connection rather than defaulted, because the failure it
119 /// prevents is silent: a client that trusted anything would reconnect
120 /// happily to whatever took the boat's address and hand it the token.
121 ///
122 /// `wake` is called from this connection's own thread once something
123 /// is worth draining -- see the module doc. A consumer with nothing
124 /// better to do than poll on a fixed timer may pass `|| {}` and get
125 /// exactly today's behaviour; passing the real thing is what lets the
126 /// timer go away.
127 #[must_use]
128 pub fn open(
129 stream_url: &str,
130 token: Option<String>,
131 trust: &Trust,
132 wake: impl Fn() + Send + 'static,
133 ) -> Self {
134 let (sender, events) = channel();
135 let stop = Arc::new(AtomicBool::new(false));
136 let pending = Arc::new(AtomicBool::new(false));
137 let sink = Sink {
138 sender,
139 pending: Arc::clone(&pending),
140 wake: Box::new(wake),
141 };
142 let (outgoing, outgoing_events) = channel();
143
144 let url = stream_url.to_owned();
145 let stopper = Arc::clone(&stop);
146 let trust = trust.clone();
147 thread::spawn(move || run(&url, token.as_deref(), &trust, &sink, &stopper, &outgoing_events));
148
149 Self {
150 events,
151 pending,
152 stop,
153 outgoing,
154 }
155 }
156
157 /// Queues `delta_json` to be sent on the wire, as-is -- building a
158 /// well-formed delta is the caller's own job (see
159 /// [`signalk::notification::mob_delta`] for the one this exists for
160 /// today). One-way: an ordinary delta carries no reply for this to
161 /// wait on, unlike [`crate::resources::Client`]'s own writes.
162 ///
163 /// # Errors
164 ///
165 /// If the background thread this would be sent from is already gone
166 /// -- the connection was dropped, or never opened.
167 pub fn publish(&self, delta_json: String) -> Result<(), ClientError> {
168 self.outgoing
169 .send(delta_json)
170 .map_err(|_| ClientError::Http("the connection is no longer running".to_owned()))
171 }
172
173 /// Whatever has arrived, without waiting.
174 ///
175 /// Drained on the consumer's own schedule rather than handed over
176 /// message by message: the consumer decides when it is ready to draw,
177 /// which is what keeps a burst of deltas from turning into a burst of
178 /// redraws. `wake` (see the module doc) is what tells it *when* that
179 /// schedule should run sooner than usual; this is still the only way
180 /// the events themselves are read.
181 ///
182 /// Marked not-pending before the drain, not after: a message arriving
183 /// *during* this call must still cause a future wake, and clearing
184 /// first means the worst case is one redundant wake, never a missed
185 /// one.
186 pub fn drain(&self) -> impl Iterator<Item = Event> + '_ {
187 self.pending.store(false, Ordering::Relaxed);
188 self.events.try_iter()
189 }
190}
191
192impl Drop for Connection {
193 fn drop(&mut self) {
194 self.stop.store(true, Ordering::Relaxed);
195 }
196}
197
198/// Where an [`Event`] goes, and the one place that decides whether that
199/// was worth a `wake`.
200///
201/// A thin wrapper around the channel rather than calling `wake` at every
202/// `send` site by hand -- which would be easy to get right once and then
203/// forget at the next call site this file grows.
204struct Sink {
205 sender: Sender<Event>,
206 pending: Arc<AtomicBool>,
207 wake: Box<dyn Fn() + Send>,
208}
209
210impl Sink {
211 /// Sends one event, waking the consumer only on the transition from
212 /// nothing pending to something pending -- once per burst, exactly the
213 /// property the module doc promises.
214 fn send(&self, event: Event) -> Result<(), ()> {
215 self.sender.send(event).map_err(|_| ())?;
216 if !self.pending.swap(true, Ordering::Relaxed) {
217 (self.wake)();
218 }
219 Ok(())
220 }
221}
222
223/// The thread: connect, read until it breaks, wait, connect again.
224fn run(url: &str, token: Option<&str>, trust: &Trust, sink: &Sink, stop: &AtomicBool, outgoing: &Receiver<String>) {
225 let mut retry = FIRST_RETRY;
226
227 while !stop.load(Ordering::Relaxed) {
228 match connect(url, token, trust) {
229 Ok(mut socket) => {
230 retry = FIRST_RETRY;
231 let reason = read_until_broken(&mut socket, sink, stop, outgoing);
232 if sink.send(Event::Disconnected { reason }).is_err() {
233 return;
234 }
235 }
236 Err(ConnectFailure::Unauthorized) => {
237 // Not worth this loop's own back-off: the socket was
238 // refused instantly, not lost to a flaky link, and
239 // retrying the same token on a timer would just refuse
240 // instantly again until the consumer -- the only side
241 // that can discard a token and ask for a new one -- acts
242 // on this event. Reported once and left to them.
243 if sink.send(Event::Unauthorized).is_err() {
244 return;
245 }
246 return;
247 }
248 Err(ConnectFailure::Other(reason)) => {
249 if sink.send(Event::Disconnected { reason }).is_err() {
250 return;
251 }
252 }
253 }
254
255 // Slept in short pieces so that dropping the connection is noticed
256 // promptly rather than after half a minute.
257 let mut waited = Duration::ZERO;
258 while waited < retry && !stop.load(Ordering::Relaxed) {
259 thread::sleep(Duration::from_millis(100));
260 waited += Duration::from_millis(100);
261 }
262 retry = (retry * 2).min(LONGEST_RETRY);
263 }
264}
265
266/// Why [`connect`] could not open a socket.
267///
268/// A plain `String` cannot be reacted to as anything but a message for
269/// the log -- and a 401 is not a message for the log, it is an
270/// instruction:
271/// the token is no longer any good, discard it and ask again. Keeping
272/// that one case structured, rather than grepping the rendered string
273/// for "401" afterwards, is what lets [`run`] tell a consumer which of
274/// the two it is looking at.
275enum ConnectFailure {
276 /// The handshake was refused with `401 Unauthorized`.
277 Unauthorized,
278 /// Anything else -- a network failure, a TLS failure, a different
279 /// HTTP status, a handshake tungstenite could not make sense of.
280 Other(String),
281}
282
283impl From<String> for ConnectFailure {
284 fn from(reason: String) -> Self {
285 Self::Other(reason)
286 }
287}
288
289/// One connection attempt.
290fn connect(
291 url: &str,
292 token: Option<&str>,
293 trust: &Trust,
294) -> Result<tungstenite::WebSocket<tungstenite::stream::MaybeTlsStream<std::net::TcpStream>>, ConnectFailure>
295{
296 // Subscribing at connect time rather than sending a subscription
297 // afterwards: it is one round trip fewer, and it means a reconnect
298 // needs no state to be replayed. "all", not "self": other vessels --
299 // AIS targets -- are read this same way now (see a client's own
300 // delta-feed handling for its Other-context branch), so this
301 // asks for every context in one subscription rather than a second
302 // one; `signalk::delta::parse`'s own "skip what is not recognised"
303 // discipline is what makes the extra traffic for paths this crate
304 // does not read at all (some other vessel's own wind data, say)
305 // harmless rather than a growing list this has to filter by hand.
306 let separator = if url.contains('?') { '&' } else { '?' };
307 let mut request = format!("{url}{separator}subscribe=all")
308 .into_client_request()
309 .map_err(|error| error.to_string())?;
310
311 if let Some(token) = token {
312 let value =
313 HeaderValue::from_str(&format!("Bearer {token}")).map_err(|error| error.to_string())?;
314 request.headers_mut().insert("Authorization", value);
315 }
316
317 // The connector is built from the boat's authority rather than left to
318 // tungstenite's default, which would be the public roots -- and no
319 // public authority can ever vouch for a server called something.local.
320 let connector = trust
321 .rustls_config()
322 .map(tungstenite::Connector::Rustls)
323 .map_err(|error| error.to_string())?;
324
325 let socket = dial(request.uri()).map_err(ConnectFailure::Other)?;
326 // Set before the TLS handshake, not after: Rustls reads through to
327 // this same underlying socket for every read it ever does, TLS
328 // handshake included, so the timeout has to be in place from the
329 // first byte -- see read_until_broken's own doc for what it is for.
330 socket
331 .set_read_timeout(Some(READ_POLL_INTERVAL))
332 .map_err(|error| ConnectFailure::Other(error.to_string()))?;
333
334 tungstenite::client_tls_with_config(request, socket, None, Some(connector))
335 .map(|(socket, _)| socket)
336 .map_err(|error| match &error {
337 // The one case worth telling apart -- see ConnectFailure's own
338 // doc. tungstenite hands back the server's real response
339 // here, so this reads its actual status rather than assuming
340 // 401 is the only reason a handshake gets refused at all.
341 // `Interrupted` never applies to a blocking handshake like
342 // this one and has no status to read, so it falls through to
343 // `Other` alongside every other failure.
344 tungstenite::HandshakeError::Failure(tungstenite::Error::Http(response))
345 if response.status() == tungstenite::http::StatusCode::UNAUTHORIZED =>
346 {
347 ConnectFailure::Unauthorized
348 }
349 _ => ConnectFailure::Other(error.to_string()),
350 })
351}
352
353/// The plain socket underneath, dialled from the request's own URI.
354///
355/// Taken from the parsed URI rather than by cutting up the URL text: the
356/// port is optional, and the host of an IPv6 URL is bracketed there and
357/// must not be when it reaches the resolver.
358fn dial(uri: &tungstenite::http::Uri) -> Result<std::net::TcpStream, String> {
359 let host = uri.host().ok_or_else(|| "no host in the URL".to_owned())?;
360 let host = host.trim_start_matches('[').trim_end_matches(']');
361
362 let port = uri.port_u16().unwrap_or(
363 match tungstenite::client::uri_mode(uri) {
364 Ok(tungstenite::stream::Mode::Tls) => 443,
365 _ => 80,
366 },
367 );
368
369 std::net::TcpStream::connect((host, port)).map_err(|error| format!("{host}:{port}: {error}"))
370}
371
372/// Reads messages until the socket fails, reporting what it understood.
373fn read_until_broken(
374 socket: &mut tungstenite::WebSocket<tungstenite::stream::MaybeTlsStream<std::net::TcpStream>>,
375 sink: &Sink,
376 stop: &AtomicBool,
377 outgoing: &Receiver<String>,
378) -> String {
379 // Learned from the greeting and then handed to every parse. Without
380 // it own-ship deltas -- which arrive under the vessel's full
381 // identifier, never under "vessels.self" -- read as another vessel,
382 // and a consumer filtering for own ship sees nothing at all.
383 let mut self_id: Option<String> = None;
384 let mut greeted = false;
385
386 while !stop.load(Ordering::Relaxed) {
387 let text = match socket.read() {
388 Ok(tungstenite::Message::Text(text)) => text.to_string(),
389 Ok(tungstenite::Message::Close(_)) => return "server closed the stream".to_owned(),
390 // Ping, pong and binary frames are not ours to interpret.
391 Ok(_) => continue,
392 // The read timeout set in connect() firing, not a real
393 // failure -- see READ_POLL_INTERVAL's own doc. This is the
394 // one moment this loop is not blocked inside socket.read(),
395 // so it is also the only place Connection::publish's own
396 // queue can actually reach the wire.
397 Err(ref error) if is_read_timeout(error) => {
398 match send_outgoing(socket, outgoing) {
399 Ok(()) => continue,
400 Err(reason) => return reason,
401 }
402 }
403 Err(error) => return error.to_string(),
404 };
405
406 // The greeting comes first and names the vessel. It is not a delta,
407 // which is why the reader has to know about it at all.
408 if !greeted {
409 greeted = true;
410 self_id = greeting_self(&text);
411 if sink
412 .send(Event::Connected {
413 self_id: self_id.clone(),
414 })
415 .is_err()
416 {
417 return "nobody is listening any more".to_owned();
418 }
419 if self_id.is_some() {
420 continue;
421 }
422 }
423
424 if let Some(delta) = signalk::parse(&text, self_id.as_deref()) {
425 if sink.send(Event::Delta(delta)).is_err() {
426 return "nobody is listening any more".to_owned();
427 }
428 }
429 }
430
431 "asked to stop".to_owned()
432}
433
434/// Whether `error` is [`connect`]'s own read timeout firing, per
435/// [`READ_POLL_INTERVAL`], rather than a real socket failure -- pulled out
436/// as a plain function so this one judgement call, the whole reason
437/// [`Connection::publish`] can ever reach the wire, is testable without a
438/// real socket.
439fn is_read_timeout(error: &tungstenite::Error) -> bool {
440 matches!(
441 error,
442 tungstenite::Error::Io(io_error)
443 if matches!(io_error.kind(), io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut)
444 )
445}
446
447/// Writes whatever [`Connection::publish`] has queued since the last time
448/// this ran, in order, stopping at the first one that fails to send --
449/// the same "something is wrong with this socket" signal a broken read
450/// already is, reported the same way rather than as some third kind of
451/// failure this crate would have to teach a caller to also react to.
452/// Called only from [`read_until_broken`]'s own read-timeout branch,
453/// never mid-frame: `socket.read()`'s own internal buffering means this
454/// is safe exactly when a read attempt has just cleanly timed out with
455/// nothing partial in flight.
456fn send_outgoing(
457 socket: &mut tungstenite::WebSocket<tungstenite::stream::MaybeTlsStream<std::net::TcpStream>>,
458 outgoing: &Receiver<String>,
459) -> Result<(), String> {
460 while let Ok(text) = outgoing.try_recv() {
461 socket.send(tungstenite::Message::text(text)).map_err(|error| error.to_string())?;
462 }
463 Ok(())
464}
465
466/// The vessel identifier out of a server greeting, if that is what this is.
467fn greeting_self(text: &str) -> Option<String> {
468 let value: serde_json::Value = serde_json::from_str(text).ok()?;
469 // A greeting names the server and the vessel and carries no updates.
470 if value.get("updates").is_some() {
471 return None;
472 }
473 value
474 .get("self")
475 .and_then(serde_json::Value::as_str)
476 .map(|id| id.trim_start_matches("vessels.").to_owned())
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn a_would_block_error_is_read_as_the_poll_timeout() {
485 let error = tungstenite::Error::Io(io::Error::from(io::ErrorKind::WouldBlock));
486 assert!(is_read_timeout(&error));
487 }
488
489 #[test]
490 fn a_timed_out_error_is_read_as_the_poll_timeout_too() {
491 // Some platforms report a blocking-socket read timeout as
492 // TimedOut rather than WouldBlock; both mean the same thing here.
493 let error = tungstenite::Error::Io(io::Error::from(io::ErrorKind::TimedOut));
494 assert!(is_read_timeout(&error));
495 }
496
497 #[test]
498 fn a_connection_reset_is_not_a_poll_timeout() {
499 let error = tungstenite::Error::Io(io::Error::from(io::ErrorKind::ConnectionReset));
500 assert!(!is_read_timeout(&error));
501 }
502
503 #[test]
504 fn a_non_io_error_is_not_a_poll_timeout() {
505 assert!(!is_read_timeout(&tungstenite::Error::AlreadyClosed));
506 }
507
508 #[test]
509 fn publishing_after_the_connection_thread_is_gone_is_an_error() {
510 let (outgoing, receiver) = channel();
511 drop(receiver);
512 let connection = Connection {
513 events: channel().1,
514 pending: Arc::new(AtomicBool::new(false)),
515 stop: Arc::new(AtomicBool::new(false)),
516 outgoing,
517 };
518 assert!(connection.publish("{}".to_owned()).is_err());
519 }
520
521 #[test]
522 fn the_greeting_names_the_vessel() {
523 // What a real server sends first, with the identifier prefixed by
524 // "vessels." -- which the delta context also carries, so it is
525 // stripped once here rather than compared for at every message.
526 let self_id = greeting_self(
527 r#"{"name":"signalk-server","version":"2.27.0",
528 "self":"vessels.urn:mrn:signalk:uuid:0000","roles":["master","main"]}"#,
529 );
530 assert_eq!(self_id.as_deref(), Some("urn:mrn:signalk:uuid:0000"));
531 }
532
533 #[test]
534 fn a_delta_is_not_mistaken_for_a_greeting() {
535 assert!(
536 greeting_self(
537 r#"{"context":"vessels.urn:mrn:signalk:uuid:0000","updates":[
538 {"$source":"n2k.1","values":[
539 {"path":"navigation.speedOverGround","value":1.0}]}]}"#
540 )
541 .is_none()
542 );
543 }
544
545 #[test]
546 fn a_greeting_without_a_vessel_is_still_a_greeting() {
547 assert!(greeting_self(r#"{"name":"signalk-server","version":"2.27.0"}"#).is_none());
548 }
549
550 fn sink() -> (Sink, Receiver<Event>, Arc<AtomicBool>) {
551 let (sender, events) = channel();
552 let pending = Arc::new(AtomicBool::new(false));
553 let woken = Arc::new(AtomicBool::new(false));
554 let sink = Sink {
555 sender,
556 pending: Arc::clone(&pending),
557 wake: {
558 let woken = Arc::clone(&woken);
559 Box::new(move || woken.store(true, Ordering::Relaxed))
560 },
561 };
562 (sink, events, woken)
563 }
564
565 #[test]
566 fn the_first_message_of_a_burst_wakes_the_consumer() {
567 let (sink, _events, woken) = sink();
568 assert!(!woken.load(Ordering::Relaxed));
569 sink.send(Event::Disconnected {
570 reason: "test".to_owned(),
571 })
572 .expect("channel is open");
573 assert!(woken.load(Ordering::Relaxed));
574 }
575
576 #[test]
577 fn a_second_message_before_the_drain_wakes_nobody_again() {
578 // The property the module doc promises: a burst wakes the consumer
579 // once, not once per message, so draining stays the only thing
580 // that decides when a redraw happens.
581 let (sink, _events, woken) = sink();
582 sink.send(Event::Disconnected {
583 reason: "one".to_owned(),
584 })
585 .unwrap();
586 woken.store(false, Ordering::Relaxed);
587
588 sink.send(Event::Disconnected {
589 reason: "two".to_owned(),
590 })
591 .unwrap();
592 assert!(!woken.load(Ordering::Relaxed), "a second wake in the same burst");
593 }
594
595 #[test]
596 fn draining_clears_pending_so_the_next_burst_can_wake_again() {
597 let (sender, events) = channel();
598 sender
599 .send(Event::Disconnected {
600 reason: "queued before drain".to_owned(),
601 })
602 .unwrap();
603
604 let connection = Connection {
605 events,
606 pending: Arc::new(AtomicBool::new(true)),
607 stop: Arc::new(AtomicBool::new(false)),
608 outgoing: channel().0,
609 };
610
611 assert_eq!(connection.drain().count(), 1);
612 // What the module doc's promise actually rests on: with pending
613 // left set, Sink::send's swap would see it already true and never
614 // call wake again, no matter how long the link then stayed quiet.
615 assert!(!connection.pending.load(Ordering::Relaxed));
616 }
617}