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