arti_client/
builder.rs

1//! Types for conveniently constructing TorClients.
2
3#![allow(missing_docs, clippy::missing_docs_in_private_items)]
4
5use crate::{
6    BootstrapBehavior, InertTorClient, Result, TorClient, TorClientConfig, err::ErrorDetail,
7};
8use std::{
9    result::Result as StdResult,
10    sync::Arc,
11    time::{Duration, Instant},
12};
13use tor_dirmgr::{DirMgrConfig, DirMgrStore};
14use tor_error::{ErrorKind, HasKind as _};
15use tor_rtcompat::Runtime;
16use tracing::instrument;
17
18/// An object that knows how to construct some kind of DirProvider.
19///
20/// Note that this type is only actually exposed when the `experimental-api`
21/// feature is enabled.
22#[allow(unreachable_pub)]
23#[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
24pub trait DirProviderBuilder<R: Runtime>: Send + Sync {
25    fn build(
26        &self,
27        runtime: R,
28        store: DirMgrStore<R>,
29        circmgr: Arc<tor_circmgr::CircMgr<R>>,
30        config: DirMgrConfig,
31    ) -> Result<Arc<dyn tor_dirmgr::DirProvider + 'static>>;
32}
33
34/// A DirProviderBuilder that constructs a regular DirMgr.
35#[derive(Clone, Debug)]
36struct DirMgrBuilder {}
37
38impl<R: Runtime> DirProviderBuilder<R> for DirMgrBuilder {
39    fn build(
40        &self,
41        runtime: R,
42        store: DirMgrStore<R>,
43        circmgr: Arc<tor_circmgr::CircMgr<R>>,
44        config: DirMgrConfig,
45    ) -> Result<Arc<dyn tor_dirmgr::DirProvider + 'static>> {
46        let dirmgr = tor_dirmgr::DirMgr::create_unbootstrapped(config, runtime, store, circmgr)
47            .map_err(ErrorDetail::DirMgrSetup)?;
48        Ok(Arc::new(dirmgr))
49    }
50}
51
52/// An object for constructing a [`TorClient`].
53///
54/// Returned by [`TorClient::builder()`].
55#[derive(Clone)]
56#[must_use]
57pub struct TorClientBuilder<R: Runtime> {
58    /// The runtime for the client to use
59    runtime: R,
60    /// The client's configuration.
61    config: TorClientConfig,
62    /// How the client should behave when it is asked to do something on the Tor
63    /// network before `bootstrap()` is called.
64    bootstrap_behavior: BootstrapBehavior,
65    /// Optional object to construct a DirProvider.
66    ///
67    /// Wrapped in an Arc so that we don't need to force DirProviderBuilder to
68    /// implement Clone.
69    dirmgr_builder: Arc<dyn DirProviderBuilder<R>>,
70    /// If present, an amount of time to wait when trying to acquire the filesystem locks for our
71    /// storage.
72    local_resource_timeout: Option<Duration>,
73    /// Optional directory filter to install for testing purposes.
74    ///
75    /// Only available when `arti-client` is built with the `dirfilter` and `experimental-api` features.
76    #[cfg(feature = "dirfilter")]
77    dirfilter: tor_dirmgr::filter::FilterConfig,
78}
79
80/// Longest allowable duration to wait for local resources to be available
81/// when creating a TorClient.
82///
83/// This value may change in future versions of Arti.
84/// It is an error to configure
85/// a [`local_resource_timeout`](TorClientBuilder)
86/// with a larger value than this.
87///
88/// (Reducing this value would count as a breaking change.)
89pub const MAX_LOCAL_RESOURCE_TIMEOUT: Duration = Duration::new(5, 0);
90
91impl<R: Runtime> TorClientBuilder<R> {
92    /// Construct a new TorClientBuilder with the given runtime.
93    pub(crate) fn new(runtime: R) -> Self {
94        Self {
95            runtime,
96            config: TorClientConfig::default(),
97            bootstrap_behavior: BootstrapBehavior::default(),
98            dirmgr_builder: Arc::new(DirMgrBuilder {}),
99            local_resource_timeout: None,
100            #[cfg(feature = "dirfilter")]
101            dirfilter: None,
102        }
103    }
104
105    /// Set the configuration for the `TorClient` under construction.
106    ///
107    /// If not called, then a compiled-in default configuration will be used.
108    pub fn config(mut self, config: TorClientConfig) -> Self {
109        self.config = config;
110        self
111    }
112
113    /// Set the bootstrap behavior for the `TorClient` under construction.
114    ///
115    /// If not called, then the default ([`BootstrapBehavior::OnDemand`]) will
116    /// be used.
117    pub fn bootstrap_behavior(mut self, bootstrap_behavior: BootstrapBehavior) -> Self {
118        self.bootstrap_behavior = bootstrap_behavior;
119        self
120    }
121
122    /// Set a timeout that we should allow when trying to acquire our local resources
123    /// (including lock files.)
124    ///
125    /// If no timeout is set, we wait for a short while (currently 500 msec) when invoked with
126    /// [`create_bootstrapped`](Self::create_bootstrapped) or
127    /// [`create_unbootstrapped_async`](Self::create_unbootstrapped_async),
128    /// and we do not wait at all if invoked with
129    /// [`create_unbootstrapped`](Self::create_unbootstrapped).
130    ///
131    /// (This difference in default behavior is meant to avoid unintentional blocking.
132    /// If you call this method, subsequent calls to `create_bootstrapped` may block
133    /// the current thread.)
134    ///
135    /// The provided timeout value may not be larger than [`MAX_LOCAL_RESOURCE_TIMEOUT`].
136    pub fn local_resource_timeout(mut self, timeout: Duration) -> Self {
137        self.local_resource_timeout = Some(timeout);
138        self
139    }
140
141    /// Override the default function used to construct the directory provider.
142    ///
143    /// Only available when compiled with the `experimental-api` feature: this
144    /// code is unstable.
145    #[cfg(all(feature = "experimental-api", feature = "error_detail"))]
146    pub fn dirmgr_builder<B>(mut self, builder: Arc<dyn DirProviderBuilder<R>>) -> Self
147    where
148        B: DirProviderBuilder<R> + 'static,
149    {
150        self.dirmgr_builder = builder;
151        self
152    }
153
154    /// Install a [`DirFilter`](tor_dirmgr::filter::DirFilter) to
155    ///
156    /// Only available when compiled with the `dirfilter` feature: this code
157    /// is unstable and not recommended for production use.
158    #[cfg(feature = "dirfilter")]
159    pub fn dirfilter<F>(mut self, filter: F) -> Self
160    where
161        F: Into<Arc<dyn tor_dirmgr::filter::DirFilter + 'static>>,
162    {
163        self.dirfilter = Some(filter.into());
164        self
165    }
166
167    /// Create a `TorClient` from this builder, without automatically launching
168    /// the bootstrap process.
169    ///
170    /// If you have left the default [`BootstrapBehavior`] in place, the client
171    /// will bootstrap itself as soon any attempt is made to use it.  You can
172    /// also bootstrap the client yourself by running its
173    /// [`bootstrap()`](TorClient::bootstrap) method.
174    ///
175    /// If you have replaced the default behavior with [`BootstrapBehavior::Manual`],
176    /// any attempts to use the client will fail with an error of kind
177    /// [`ErrorKind::BootstrapRequired`],
178    /// until you have called [`TorClient::bootstrap`] yourself.
179    /// This option is useful if you wish to have control over the bootstrap
180    /// process (for example, you might wish to avoid initiating network
181    /// connections until explicit user confirmation is given).
182    ///
183    /// If a [local_resource_timeout](Self::local_resource_timeout) has been set, this function may
184    /// block the current thread.
185    /// Use [`create_unbootstrapped_async`](Self::create_unbootstrapped_async)
186    /// if that is not what you want.
187    #[instrument(skip_all, level = "trace")]
188    pub fn create_unbootstrapped(&self) -> Result<TorClient<R>> {
189        let timeout = self.local_resource_timeout_or(Duration::from_millis(0))?;
190        let give_up_at = Instant::now() + timeout;
191        let mut first_attempt = true;
192
193        loop {
194            match self.create_unbootstrapped_inner(Instant::now, give_up_at, first_attempt) {
195                Err(delay) => {
196                    first_attempt = false;
197                    std::thread::sleep(delay);
198                }
199                Ok(other) => return other,
200            }
201        }
202    }
203
204    /// Like create_unbootstrapped, but does not block the thread while trying to acquire the lock.
205    ///
206    /// If no [`local_resource_timeout`](Self::local_resource_timeout) has been set, this function may
207    /// delay a short while (currently 500 msec) for local resources (such as lock files) to be available.
208    /// Set `local_resource_timeout` to 0 if you do not want this behavior.
209    #[instrument(skip_all, level = "trace")]
210    pub async fn create_unbootstrapped_async(&self) -> Result<TorClient<R>> {
211        // TODO: This code is largely duplicated from create_unbootstrapped above.  It might be good
212        // to have a single shared implementation to handle both the sync and async cases, but I am
213        // concerned that doing so would just add a lot of complexity.
214        let timeout = self.local_resource_timeout_or(Duration::from_millis(500))?;
215        let give_up_at = self.runtime.now() + timeout;
216        let mut first_attempt = true;
217
218        loop {
219            match self.create_unbootstrapped_inner(|| self.runtime.now(), give_up_at, first_attempt)
220            {
221                Err(delay) => {
222                    first_attempt = false;
223                    self.runtime.sleep(delay).await;
224                }
225                Ok(other) => return other,
226            }
227        }
228    }
229
230    /// Helper for create_bootstrapped and create_bootstrapped_async.
231    ///
232    /// Does not retry on `LocalResourceAlreadyInUse`; instead, returns a time that we should wait,
233    /// and log a message if `first_attempt` is true.
234    #[instrument(skip_all, level = "trace")]
235    fn create_unbootstrapped_inner<F>(
236        &self,
237        now: F,
238        give_up_at: Instant,
239        first_attempt: bool,
240    ) -> StdResult<Result<TorClient<R>>, Duration>
241    where
242        F: FnOnce() -> Instant,
243    {
244        #[allow(unused_mut)]
245        let mut dirmgr_extensions = tor_dirmgr::config::DirMgrExtensions::default();
246        #[cfg(feature = "dirfilter")]
247        {
248            dirmgr_extensions.filter.clone_from(&self.dirfilter);
249        }
250
251        let result: Result<TorClient<R>> = TorClient::create_inner(
252            self.runtime.clone(),
253            &self.config,
254            self.bootstrap_behavior,
255            self.dirmgr_builder.as_ref(),
256            dirmgr_extensions,
257        )
258        .map_err(ErrorDetail::into);
259
260        match result {
261            Err(e) if e.kind() == ErrorKind::LocalResourceAlreadyInUse => {
262                let now = now();
263                if now >= give_up_at {
264                    // no time remaining; return the error that we got.
265                    Ok(Err(e))
266                } else {
267                    let remaining = give_up_at.saturating_duration_since(now);
268                    if first_attempt {
269                        tracing::info!(
270                            "Looks like another TorClient may be running; retrying for up to {}",
271                            humantime::Duration::from(remaining),
272                        );
273                    }
274                    // We'll retry at least once.
275                    // TODO: Maybe use a smarter backoff strategy here?
276                    Err(Duration::from_millis(50).min(remaining))
277                }
278            }
279            // We either succeeded, or failed for a reason other than LocalResourceAlreadyInUse
280            other => Ok(other),
281        }
282    }
283
284    /// Create a TorClient from this builder, and try to bootstrap it.
285    pub async fn create_bootstrapped(&self) -> Result<TorClient<R>> {
286        let r = self.create_unbootstrapped_async().await?;
287        r.bootstrap().await?;
288        Ok(r)
289    }
290
291    /// Return the local_resource_timeout, or `dflt` if none is defined.
292    ///
293    /// Give an error if the value is above MAX_LOCAL_RESOURCE_TIMEOUT
294    fn local_resource_timeout_or(&self, dflt: Duration) -> Result<Duration> {
295        let timeout = self.local_resource_timeout.unwrap_or(dflt);
296        if timeout > MAX_LOCAL_RESOURCE_TIMEOUT {
297            return Err(
298                ErrorDetail::Configuration(tor_config::ConfigBuildError::Invalid {
299                    field: "local_resource_timeout".into(),
300                    problem: "local resource timeout too large".into(),
301                })
302                .into(),
303            );
304        }
305        Ok(timeout)
306    }
307
308    /// Create an `InertTorClient` from this builder, without launching
309    /// the bootstrap process, or connecting to the network.
310    ///
311    /// It is currently unspecified whether constructing an `InertTorClient`
312    /// will hold any locks that prevent opening a `TorClient` with the same
313    /// directories.
314    //
315    // TODO(#1576): reach a decision here.
316    #[allow(clippy::unnecessary_wraps)]
317    pub fn create_inert(&self) -> Result<InertTorClient> {
318        Ok(InertTorClient::new(&self.config)?)
319    }
320}
321
322#[cfg(test)]
323mod test {
324    use tor_rtcompat::PreferredRuntime;
325
326    use super::*;
327
328    fn must_be_send_and_sync<S: Send + Sync>() {}
329
330    #[test]
331    fn builder_is_send() {
332        must_be_send_and_sync::<TorClientBuilder<PreferredRuntime>>();
333    }
334}