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