1
//! Code to collect and publish information about a client's bootstrapping
2
//! status.
3

            
4
use std::{borrow::Cow, fmt, time::SystemTime};
5

            
6
use derive_more::Display;
7
use educe::Educe;
8
use futures::{Stream, StreamExt};
9
use tor_basic_utils::skip_fmt;
10
use tor_chanmgr::{ConnBlockage, ConnStatus, ConnStatusEvents};
11
use tor_dirmgr::DirBootstrapStatus;
12
use tracing::debug;
13

            
14
/// Information about how ready a [`crate::TorClient`] is to handle requests.
15
///
16
/// Note that this status does not change monotonically: a `TorClient` can
17
/// become more _or less_ bootstrapped over time. (For example, a client can
18
/// become less bootstrapped if it loses its internet connectivity, or if its
19
/// directory information expires before it's able to replace it.)
20
//
21
// # Note
22
//
23
// We need to keep this type fairly small, since it will get cloned whenever
24
// it's observed on a stream.   If it grows large, we can add an Arc<> around
25
// its data.
26
2
#[derive(Debug, Clone, Default)]
27
pub struct BootstrapStatus {
28
    /// Status for our connection to the tor network
29
    conn_status: ConnStatus,
30
    /// Status for our directory information.
31
    dir_status: DirBootstrapStatus,
32
}
33

            
34
impl BootstrapStatus {
35
    /// Return a rough fraction (from 0.0 to 1.0) representing how far along
36
    /// the client's bootstrapping efforts are.
37
    ///
38
    /// 0 is defined as "just started"; 1 is defined as "ready to use."
39
    pub fn as_frac(&self) -> f32 {
40
        // Coefficients chosen arbitrarily.
41
        self.conn_status.frac() * 0.15 + self.dir_status.frac_at(SystemTime::now()) * 0.85
42
    }
43

            
44
    /// Return true if the status indicates that the client is ready for
45
    /// traffic.
46
    ///
47
    /// For the purposes of this function, the client is "ready for traffic" if,
48
    /// as far as we know, we can start acting on a new client request immediately.
49
    pub fn ready_for_traffic(&self) -> bool {
50
        let now = SystemTime::now();
51
        self.conn_status.usable() && self.dir_status.usable_at(now)
52
    }
53

            
54
    /// If the client is unable to make forward progress for some reason, return
55
    /// that reason.
56
    ///
57
    /// (Returns None if the client doesn't seem to be stuck.)
58
    ///
59
    /// # Caveats
60
    ///
61
    /// This function provides a "best effort" diagnostic: there
62
    /// will always be some blockage types that it can't diagnose
63
    /// correctly.  It may declare that Arti is stuck for reasons that
64
    /// are incorrect; or it may declare that the client is not stuck
65
    /// when in fact no progress is being made.
66
    ///
67
    /// Therefore, the caller should always use a certain amount of
68
    /// modesty when reporting these values to the user. For example,
69
    /// it's probably better to say "Arti says it's stuck because it
70
    /// can't make connections to the internet" rather than "You are
71
    /// not on the internet."
72
    pub fn blocked(&self) -> Option<Blockage> {
73
        if let Some(b) = self.conn_status.blockage() {
74
            let message = b.to_string().into();
75
            let kind = b.into();
76
            Some(Blockage { kind, message })
77
        } else {
78
            None
79
        }
80
    }
81

            
82
    /// Adjust this status based on new connection-status information.
83
2
    fn apply_conn_status(&mut self, status: ConnStatus) {
84
2
        self.conn_status = status;
85
2
    }
86

            
87
    /// Adjust this status based on new directory-status information.
88
2
    fn apply_dir_status(&mut self, status: DirBootstrapStatus) {
89
2
        self.dir_status = status;
90
2
    }
91
}
92

            
93
/// A reason why a client believes it is stuck.
94
#[derive(Clone, Debug, Display)]
95
#[display(fmt = "{} ({})", "kind", "message")]
96
pub struct Blockage {
97
    /// Why do we think we're blocked?
98
    kind: BlockageKind,
99
    /// A human-readable message about the blockage.
100
    message: Cow<'static, str>,
101
}
102

            
103
/// A specific type of blockage that a client believes it is experiencing.
104
///
105
/// Used to distinguish among instances of [`Blockage`].
106
#[derive(Clone, Debug, Display)]
107
#[non_exhaustive]
108
pub enum BlockageKind {
109
    /// There is some kind of problem with connecting to the network.
110
    #[display(fmt = "We seem to be offline")]
111
    Offline,
112
    /// We can connect, but our connections seem to be filtered.
113
    #[display(fmt = "Our internet connection seems filtered")]
114
    Filtering,
115
    /// We have some other kind of problem connecting to Tor
116
    #[display(fmt = "Can't reach the Tor network")]
117
    CantReachTor,
118
}
119

            
120
impl From<ConnBlockage> for BlockageKind {
121
    fn from(b: ConnBlockage) -> BlockageKind {
122
        match b {
123
            ConnBlockage::NoTcp => BlockageKind::Offline,
124
            ConnBlockage::NoHandshake => BlockageKind::Filtering,
125
            _ => BlockageKind::CantReachTor,
126
        }
127
    }
128
}
129

            
130
impl fmt::Display for BootstrapStatus {
131
    /// Format this [`BootstrapStatus`].
132
    ///
133
    /// Note that the string returned by this function is designed for human
134
    /// readability, not for machine parsing.  Other code *should not* depend
135
    /// on particular elements of this string.
136
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137
        let percent = (self.as_frac() * 100.0).round() as u32;
138
        if let Some(problem) = self.blocked() {
139
            write!(f, "Stuck at {}%: {}", percent, problem)
140
        } else {
141
            write!(
142
                f,
143
                "{}%: {}; {}",
144
                percent, &self.conn_status, &self.dir_status
145
            )
146
        }
147
    }
148
}
149

            
150
/// Task that runs forever, updating a client's status via the provided
151
/// `sender`.
152
///
153
/// TODO(nickm): Eventually this will use real stream of events to see when we
154
/// are bootstrapped or not.  For now, it just says that we're not-ready until
155
/// the given Receiver fires.
156
///
157
/// TODO(nickm): This should eventually close the stream when the client is
158
/// dropped.
159
2
pub(crate) async fn report_status(
160
2
    mut sender: postage::watch::Sender<BootstrapStatus>,
161
2
    conn_status: ConnStatusEvents,
162
2
    dir_status: impl Stream<Item = DirBootstrapStatus> + Unpin,
163
2
) {
164
2
    /// Internal enumeration to combine incoming status changes.
165
2
    enum Event {
166
2
        /// A connection status change
167
2
        Conn(ConnStatus),
168
2
        /// A directory status change
169
2
        Dir(DirBootstrapStatus),
170
2
    }
171
2
    let mut stream =
172
2
        futures::stream::select(conn_status.map(Event::Conn), dir_status.map(Event::Dir));
173

            
174
6
    while let Some(event) = stream.next().await {
175
4
        let mut b = sender.borrow_mut();
176
4
        match event {
177
2
            Event::Conn(e) => b.apply_conn_status(e),
178
2
            Event::Dir(e) => b.apply_dir_status(e),
179
        }
180
        debug!("{}", *b);
181
    }
182
}
183

            
184
/// A [`Stream`] of [`BootstrapStatus`] events.
185
///
186
/// This stream isn't guaranteed to receive every change in bootstrap status; if
187
/// changes happen more frequently than the receiver can observe, some of them
188
/// will be dropped.
189
//
190
// Note: We use a wrapper type around watch::Receiver here, in order to hide its
191
// implementation type.  We do that because we might want to change the type in
192
// the future, and because some of the functionality exposed by Receiver (like
193
// `borrow()` and the postage::Stream trait) are extraneous to the API we want.
194
#[derive(Clone, Educe)]
195
#[educe(Debug)]
196
pub struct BootstrapEvents {
197
    /// The receiver that implements this stream.
198
    #[educe(Debug(method = "skip_fmt"))]
199
    pub(crate) inner: postage::watch::Receiver<BootstrapStatus>,
200
}
201

            
202
impl Stream for BootstrapEvents {
203
    type Item = BootstrapStatus;
204

            
205
    fn poll_next(
206
        mut self: std::pin::Pin<&mut Self>,
207
        cx: &mut std::task::Context<'_>,
208
    ) -> std::task::Poll<Option<Self::Item>> {
209
        self.inner.poll_next_unpin(cx)
210
    }
211
}