Skip to main content

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}