libdd_crashtracker/shared/configuration/
builder.rs1use crate::{default_signals, shared::constants, signal_from_signum};
4use alloc::borrow::Cow;
5use core::time::Duration;
6use libdd_common::Endpoint;
7
8use super::{default_max_threads, CrashtrackerConfiguration, StacktraceCollection};
9
10#[derive(Debug, Default)]
11pub struct CrashtrackerConfigurationBuilder {
12 additional_files: Vec<String>,
13 collect_all_threads: bool,
14 create_alt_stack: bool,
15 demangle_names: bool,
16 endpoint_url: Option<String>,
17 endpoint_api_key: Option<String>,
18 endpoint_timeout_ms: Option<u64>,
19 endpoint_test_token: Option<String>,
20 endpoint_use_system_resolver: bool,
21 max_threads: Option<usize>,
22 resolve_frames: StacktraceCollection,
23 signals: Vec<i32>,
24 timeout: Option<Duration>,
25 unix_socket_path: Option<String>,
26 #[cfg(unix)]
27 unix_socket_connector: Option<fn(&str) -> std::os::fd::RawFd>,
28 use_alt_stack: bool,
29}
30
31impl CrashtrackerConfigurationBuilder {
32 pub fn additional_files(mut self, files: Vec<String>) -> Self {
33 self.additional_files = files;
34 self
35 }
36
37 pub fn collect_all_threads(mut self, collect: bool) -> Self {
38 self.collect_all_threads = collect;
39 self
40 }
41
42 pub fn create_alt_stack(mut self, create: bool) -> Self {
43 self.create_alt_stack = create;
44 self
45 }
46
47 pub fn use_alt_stack(mut self, use_it: bool) -> Self {
48 self.use_alt_stack = use_it;
49 self
50 }
51
52 pub fn demangle_names(mut self, demangle: bool) -> Self {
53 self.demangle_names = demangle;
54 self
55 }
56
57 pub fn endpoint_url(mut self, url: &str) -> Self {
58 if !url.is_empty() {
59 self.endpoint_url = Some(url.to_string());
60 }
61 self
62 }
63
64 pub fn endpoint_api_key(mut self, api_key: &str) -> Self {
65 self.endpoint_api_key = Some(api_key.to_string());
66 self
67 }
68
69 pub fn endpoint_timeout_ms(mut self, timeout_ms: u64) -> Self {
70 self.endpoint_timeout_ms = Some(timeout_ms);
71 self
72 }
73
74 pub fn endpoint_test_token(mut self, test_token: &str) -> Self {
75 self.endpoint_test_token = Some(test_token.to_string());
76 self
77 }
78
79 pub fn endpoint_use_system_resolver(mut self, use_system_resolver: bool) -> Self {
80 self.endpoint_use_system_resolver = use_system_resolver;
81 self
82 }
83
84 pub fn max_threads(mut self, max: usize) -> Self {
85 self.max_threads = Some(max);
86 self
87 }
88
89 pub fn resolve_frames(mut self, resolve: StacktraceCollection) -> Self {
90 self.resolve_frames = resolve;
91 self
92 }
93
94 pub fn signals(mut self, signals: Vec<i32>) -> Self {
95 self.signals = signals;
96 self
97 }
98
99 pub fn timeout(mut self, timeout: Duration) -> Self {
100 self.timeout = Some(timeout);
101 self
102 }
103
104 pub fn unix_socket_path(mut self, path: String) -> Self {
105 self.unix_socket_path = Some(path);
106 self
107 }
108
109 pub fn unix_socket_connector(mut self, connector: fn(&str) -> std::os::fd::RawFd) -> Self {
110 self.unix_socket_connector = Some(connector);
111 self
112 }
113
114 pub fn build(self) -> anyhow::Result<CrashtrackerConfiguration> {
115 anyhow::ensure!(
117 !self.create_alt_stack || self.use_alt_stack,
118 "Cannot create an altstack without using it"
119 );
120 let timeout = self
121 .timeout
122 .unwrap_or(constants::DD_CRASHTRACK_DEFAULT_TIMEOUT);
123 let endpoint = self
124 .endpoint_url
125 .map(|url| {
126 Ok::<Endpoint, anyhow::Error>(Endpoint {
127 url: libdd_common::parse_uri(&url)?,
128 api_key: self.endpoint_api_key.map(Cow::Owned),
129 timeout_ms: self
130 .endpoint_timeout_ms
131 .unwrap_or(Endpoint::DEFAULT_TIMEOUT),
132 test_token: self.endpoint_test_token.map(Cow::Owned),
133 use_system_resolver: self.endpoint_use_system_resolver,
134 })
135 })
136 .transpose()?;
137
138 let mut signals = self.signals;
139 if signals.is_empty() {
140 signals = default_signals();
141 } else {
142 let before_len = signals.len();
144 signals.sort();
145 signals.dedup();
146 anyhow::ensure!(
147 before_len == signals.len(),
148 "Signals contained duplicate elements"
149 );
150 signals
152 .iter()
153 .try_for_each(|x| signal_from_signum(*x).map(|_| ()))?;
154 }
155
156 Ok(CrashtrackerConfiguration {
159 additional_files: self.additional_files,
160 collect_all_threads: self.collect_all_threads,
161 create_alt_stack: self.create_alt_stack,
162 use_alt_stack: self.use_alt_stack,
163 endpoint,
164 max_threads: self.max_threads.unwrap_or(default_max_threads()),
165 resolve_frames: self.resolve_frames,
166 signals,
167 timeout,
168 unix_socket_path: self.unix_socket_path,
169 unix_socket_connector: self
170 .unix_socket_connector
171 .unwrap_or(super::default_unix_socket_connector),
172 demangle_names: self.demangle_names,
173 })
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::{default_signals, shared::constants};
181 use core::time::Duration;
182
183 #[test]
184 fn test_build_defaults() -> anyhow::Result<()> {
185 let config = CrashtrackerConfiguration::builder().build()?;
186 assert!(config.additional_files().is_empty());
187 assert!(!config.create_alt_stack());
188 assert!(!config.use_alt_stack());
189 assert!(!config.demangle_names());
190 assert!(config.endpoint().is_none());
191 assert_eq!(config.resolve_frames(), StacktraceCollection::Disabled);
192 assert_eq!(config.signals(), &default_signals());
193 assert_eq!(config.timeout(), constants::DD_CRASHTRACK_DEFAULT_TIMEOUT);
194 assert!(config.unix_socket_path().is_none());
195 Ok(())
196 }
197
198 #[test]
199 fn test_create_alt_stack_without_use_fails() {
200 let result = CrashtrackerConfiguration::builder()
201 .create_alt_stack(true)
202 .use_alt_stack(false)
203 .build();
204 assert!(result.is_err());
205 }
206
207 #[test]
208 fn test_create_and_use_alt_stack_succeeds() -> anyhow::Result<()> {
209 let config = CrashtrackerConfiguration::builder()
210 .create_alt_stack(true)
211 .use_alt_stack(true)
212 .build()?;
213 assert!(config.create_alt_stack());
214 assert!(config.use_alt_stack());
215 Ok(())
216 }
217
218 #[test]
219 fn test_endpoint_empty_url() -> anyhow::Result<()> {
220 let config = CrashtrackerConfiguration::builder()
221 .endpoint_url("")
222 .build()?;
223 assert!(config.endpoint().is_none());
224 Ok(())
225 }
226
227 #[test]
228 fn test_endpoint_file_url() -> anyhow::Result<()> {
229 let config = CrashtrackerConfiguration::builder()
230 .endpoint_url("file:///tmp/crashreport.json")
231 .build()?;
232 let endpoint = config.endpoint().as_ref().expect("endpoint should be set");
233 assert_eq!(endpoint.url.scheme_str(), Some("file"));
234 Ok(())
235 }
236
237 #[test]
238 fn test_endpoint_http_url() -> anyhow::Result<()> {
239 let config = CrashtrackerConfiguration::builder()
240 .endpoint_url("http://localhost:8126/api/v2/profile")
241 .build()?;
242 let endpoint = config.endpoint().as_ref().expect("endpoint should be set");
243 assert_eq!(endpoint.url.scheme_str(), Some("http"));
244 assert_eq!(endpoint.url.port().unwrap().as_u16(), 8126);
245 assert_eq!(endpoint.url.host(), Some("localhost"));
246 Ok(())
247 }
248
249 #[test]
250 fn test_endpoint_default_timeout_ms() -> anyhow::Result<()> {
251 let config = CrashtrackerConfiguration::builder()
252 .endpoint_url("http://localhost:8126")
253 .build()?;
254 let endpoint = config.endpoint().as_ref().expect("endpoint should be set");
255 assert_eq!(endpoint.url.port().unwrap().as_u16(), 8126);
256 assert_eq!(endpoint.timeout_ms, Endpoint::DEFAULT_TIMEOUT);
257 Ok(())
258 }
259
260 #[test]
261 fn test_endpoint_custom_timeout_ms() -> anyhow::Result<()> {
262 let config = CrashtrackerConfiguration::builder()
263 .endpoint_url("http://localhost:8126")
264 .endpoint_timeout_ms(1234)
265 .build()?;
266 let endpoint = config.endpoint().as_ref().expect("endpoint should be set");
267 assert_eq!(endpoint.timeout_ms, 1234);
268 Ok(())
269 }
270
271 #[test]
272 fn test_endpoint_with_api_key() -> anyhow::Result<()> {
273 let config = CrashtrackerConfiguration::builder()
274 .endpoint_url("http://localhost:8126")
275 .endpoint_api_key("my-api-key")
276 .build()?;
277 let endpoint = config.endpoint().as_ref().expect("endpoint should be set");
278 assert_eq!(endpoint.api_key.as_deref(), Some("my-api-key"));
279 Ok(())
280 }
281
282 #[test]
283 fn test_endpoint_with_test_token() -> anyhow::Result<()> {
284 let config = CrashtrackerConfiguration::builder()
285 .endpoint_url("http://localhost:8126")
286 .endpoint_test_token("test-token")
287 .build()?;
288 let endpoint = config.endpoint().as_ref().expect("endpoint should be set");
289 assert_eq!(endpoint.test_token.as_deref(), Some("test-token"));
290 Ok(())
291 }
292
293 #[test]
294 fn test_endpoint_with_system_resolver() -> anyhow::Result<()> {
295 let config = CrashtrackerConfiguration::builder()
296 .endpoint_url("http://localhost:8126")
297 .endpoint_use_system_resolver(true)
298 .build()?;
299 let endpoint = config.endpoint().as_ref().expect("endpoint should be set");
300 assert!(endpoint.use_system_resolver);
301 Ok(())
302 }
303
304 #[test]
305 fn test_signals_default() -> anyhow::Result<()> {
306 let config = CrashtrackerConfiguration::builder().build()?;
307 assert_eq!(config.signals(), &default_signals());
308 Ok(())
309 }
310
311 #[test]
312 fn test_signals_custom() -> anyhow::Result<()> {
313 let signals = vec![libc::SIGSEGV, libc::SIGBUS];
314 let config = CrashtrackerConfiguration::builder()
315 .signals(signals.clone())
316 .build()?;
317 let mut expected = signals;
318 expected.sort();
319 assert_eq!(config.signals(), &expected);
320 Ok(())
321 }
322
323 #[test]
324 fn test_signals_duplicates_fail() {
325 let result = CrashtrackerConfiguration::builder()
326 .signals(vec![libc::SIGSEGV, libc::SIGSEGV])
327 .build();
328 assert!(result.is_err());
329 }
330
331 #[test]
332 fn test_signals_invalid_fail() {
333 let result = CrashtrackerConfiguration::builder()
334 .signals(vec![9999])
335 .build();
336 assert!(result.is_err());
337 }
338
339 #[test]
340 fn test_timeout_default() -> anyhow::Result<()> {
341 let config = CrashtrackerConfiguration::builder().build()?;
342 assert_eq!(config.timeout(), constants::DD_CRASHTRACK_DEFAULT_TIMEOUT);
343 Ok(())
344 }
345
346 #[test]
347 fn test_timeout_custom() -> anyhow::Result<()> {
348 let config = CrashtrackerConfiguration::builder()
349 .timeout(Duration::from_secs(10))
350 .build()?;
351 assert_eq!(config.timeout(), Duration::from_secs(10));
352 Ok(())
353 }
354
355 #[test]
356 fn test_additional_files() -> anyhow::Result<()> {
357 let files = vec!["/tmp/file1.txt".to_string(), "/tmp/file2.txt".to_string()];
358 let config = CrashtrackerConfiguration::builder()
359 .additional_files(files.clone())
360 .build()?;
361 assert_eq!(config.additional_files(), &files);
362 Ok(())
363 }
364
365 #[test]
366 fn test_demangle_names() -> anyhow::Result<()> {
367 let config = CrashtrackerConfiguration::builder()
368 .demangle_names(true)
369 .build()?;
370 assert!(config.demangle_names());
371 Ok(())
372 }
373
374 #[test]
375 fn test_resolve_frames() -> anyhow::Result<()> {
376 for variant in [
377 StacktraceCollection::Disabled,
378 StacktraceCollection::WithoutSymbols,
379 StacktraceCollection::EnabledWithInprocessSymbols,
380 StacktraceCollection::EnabledWithSymbolsInReceiver,
381 ] {
382 let config = CrashtrackerConfiguration::builder()
383 .resolve_frames(variant)
384 .build()?;
385 assert_eq!(config.resolve_frames(), variant);
386 }
387 Ok(())
388 }
389
390 #[test]
391 fn test_unix_socket_path() -> anyhow::Result<()> {
392 let config = CrashtrackerConfiguration::builder()
393 .unix_socket_path("/tmp/crashtracker.sock".to_string())
394 .build()?;
395 assert_eq!(
396 config.unix_socket_path(),
397 &Some("/tmp/crashtracker.sock".to_string())
398 );
399 Ok(())
400 }
401}