crashpad_rs/
config.rs

1#[cfg(not(any(target_os = "ios", target_os = "tvos", target_os = "watchos")))]
2use crate::CrashpadError;
3use crate::Result;
4use std::env;
5use std::path::{Path, PathBuf};
6
7/// Configuration for Crashpad client
8#[derive(Debug, Clone)]
9pub struct CrashpadConfig {
10    handler_path: PathBuf,
11    database_path: PathBuf,
12    metrics_path: PathBuf,
13    url: Option<String>,
14    handler_arguments: Vec<String>,
15}
16
17impl Default for CrashpadConfig {
18    fn default() -> Self {
19        let exe_dir = env::current_exe()
20            .ok()
21            .and_then(|p| p.parent().map(|p| p.to_path_buf()))
22            .unwrap_or_else(|| PathBuf::from("."));
23
24        Self {
25            handler_path: PathBuf::new(),
26            database_path: exe_dir.join("crashpad_db"),
27            metrics_path: exe_dir.join("crashpad_metrics"),
28            url: None,
29            handler_arguments: Vec::new(),
30        }
31    }
32}
33
34impl CrashpadConfig {
35    /// Create a new configuration with default values
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Create a builder for the configuration
41    pub fn builder() -> CrashpadConfigBuilder {
42        CrashpadConfigBuilder::default()
43    }
44
45    /// Set the database path
46    pub fn with_database_path<P: AsRef<Path>>(mut self, path: P) -> Self {
47        self.database_path = path.as_ref().to_path_buf();
48        self
49    }
50
51    /// Set the metrics path
52    pub fn with_metrics_path<P: AsRef<Path>>(mut self, path: P) -> Self {
53        self.metrics_path = path.as_ref().to_path_buf();
54        self
55    }
56
57    /// Set the upload URL
58    pub fn with_url<S: Into<String>>(mut self, url: S) -> Self {
59        self.url = Some(url.into());
60        self
61    }
62
63    /// Get the handler path
64    ///
65    /// Search order:
66    /// 1. Path specified in config (if provided)
67    /// 2. CRASHPAD_HANDLER environment variable
68    /// 3. Same directory as the executable
69    /// 4. Current working directory
70    pub(crate) fn handler_path(&self) -> Result<PathBuf> {
71        // iOS/tvOS/watchOS use in-process handler, no external handler needed
72        #[cfg(any(target_os = "ios", target_os = "tvos", target_os = "watchos"))]
73        {
74            // Return empty path for iOS - it's handled in-process
75            return Ok(PathBuf::new());
76        }
77
78        #[cfg(not(any(target_os = "ios", target_os = "tvos", target_os = "watchos")))]
79        {
80            // Determine handler filename based on platform
81            let handler_name = if cfg!(target_os = "android") {
82                "libcrashpad_handler.so"
83            } else if cfg!(windows) {
84                "crashpad_handler.exe"
85            } else {
86                "crashpad_handler"
87            };
88
89            // 1. Check if path was explicitly set in config
90            if !self.handler_path.as_os_str().is_empty() {
91                let path = &self.handler_path;
92                if path.exists() {
93                    return Ok(path.clone());
94                }
95                // If explicitly set but doesn't exist, still return it
96                // (let the caller handle the error for better diagnostics)
97                return Ok(path.clone());
98            }
99
100            // 2. Check CRASHPAD_HANDLER environment variable
101            if let Ok(env_path) = env::var("CRASHPAD_HANDLER") {
102                let path = PathBuf::from(env_path);
103                if path.exists() {
104                    return Ok(path);
105                }
106            }
107
108            // 3. Check same directory as executable
109            if let Ok(exe_path) = env::current_exe() {
110                if let Some(exe_dir) = exe_path.parent() {
111                    let handler_path = exe_dir.join(handler_name);
112                    if handler_path.exists() {
113                        return Ok(handler_path);
114                    }
115                }
116            }
117
118            // 4. Check current working directory
119            let cwd_handler = PathBuf::from(handler_name);
120            if cwd_handler.exists() {
121                return Ok(cwd_handler);
122            }
123
124            Err(CrashpadError::InvalidConfiguration(
125                format!(
126                    "Handler '{handler_name}' not found. Searched: config path, CRASHPAD_HANDLER env, executable directory, current directory"
127                )
128            ))
129        }
130    }
131
132    pub(crate) fn database_path(&self) -> &Path {
133        &self.database_path
134    }
135
136    pub(crate) fn metrics_path(&self) -> &Path {
137        &self.metrics_path
138    }
139
140    pub(crate) fn url(&self) -> Option<&str> {
141        self.url.as_deref()
142    }
143
144    pub(crate) fn handler_arguments(&self) -> &[String] {
145        &self.handler_arguments
146    }
147}
148
149/// Builder for CrashpadConfig
150#[derive(Default)]
151pub struct CrashpadConfigBuilder {
152    config: CrashpadConfig,
153}
154
155impl CrashpadConfigBuilder {
156    /// Set the handler path
157    pub fn handler_path<P: AsRef<Path>>(mut self, path: P) -> Self {
158        self.config.handler_path = path.as_ref().to_path_buf();
159        self
160    }
161
162    /// Set the database path
163    pub fn database_path<P: AsRef<Path>>(mut self, path: P) -> Self {
164        self.config.database_path = path.as_ref().to_path_buf();
165        self
166    }
167
168    /// Set the metrics path
169    pub fn metrics_path<P: AsRef<Path>>(mut self, path: P) -> Self {
170        self.config.metrics_path = path.as_ref().to_path_buf();
171        self
172    }
173
174    /// Set the upload URL
175    pub fn url<S: Into<String>>(mut self, url: S) -> Self {
176        self.config.url = Some(url.into());
177        self
178    }
179
180    /// Control upload rate limiting
181    ///
182    /// Limits crash report uploads to one per hour when enabled.
183    ///
184    /// # Platform Behavior
185    /// - **Desktop/Linux/Android**: Passed as handler process argument
186    /// - **iOS/tvOS/watchOS**: Currently ignored (hardcoded to false in Crashpad)
187    ///
188    /// # Default
189    /// `true` - Rate limiting enabled
190    pub fn rate_limit(mut self, enabled: bool) -> Self {
191        if !enabled {
192            self.config
193                .handler_arguments
194                .push("--no-rate-limit".to_string());
195        }
196        self
197    }
198
199    /// Control gzip compression for uploads
200    ///
201    /// # Platform Behavior
202    /// - **Desktop/Linux/Android**: Passed as handler process argument
203    /// - **iOS/tvOS/watchOS**: Currently ignored (hardcoded to true in Crashpad)
204    ///
205    /// # Default
206    /// `true` - Gzip compression enabled
207    pub fn upload_gzip(mut self, enabled: bool) -> Self {
208        if !enabled {
209            self.config
210                .handler_arguments
211                .push("--no-upload-gzip".to_string());
212        }
213        self
214    }
215
216    /// Control periodic database maintenance tasks
217    ///
218    /// # Platform Behavior
219    /// - **Desktop/Linux/Android**: Passed as handler process argument
220    /// - **iOS/tvOS/watchOS**: Currently ignored (uses internal pruning thread)
221    ///
222    /// # Default
223    /// `true` - Periodic tasks enabled
224    pub fn periodic_tasks(mut self, enabled: bool) -> Self {
225        if !enabled {
226            self.config
227                .handler_arguments
228                .push("--no-periodic-tasks".to_string());
229        }
230        self
231    }
232
233    /// Control client identification via URL
234    ///
235    /// # Platform Behavior
236    /// - **Desktop/Linux/Android**: Passed as handler process argument
237    /// - **iOS/tvOS/watchOS**: Currently ignored (hardcoded to true in Crashpad)
238    ///
239    /// # Default
240    /// `true` - Client identification enabled
241    pub fn identify_client_via_url(mut self, enabled: bool) -> Self {
242        if !enabled {
243            self.config
244                .handler_arguments
245                .push("--no-identify-client-via-url".to_string());
246        }
247        self
248    }
249
250    /// Add a custom handler argument (advanced usage)
251    ///
252    /// # Platform Behavior
253    /// - **Desktop/Linux/Android**: Passed to handler process
254    /// - **iOS/tvOS/watchOS**: Ignored
255    ///
256    /// # Example
257    /// ```rust
258    /// # use crashpad_rs::CrashpadConfig;
259    /// let config = CrashpadConfig::builder()
260    ///     .handler_argument("--monitor-self")
261    ///     .build();
262    /// ```
263    pub fn handler_argument<S: Into<String>>(mut self, arg: S) -> Self {
264        self.config.handler_arguments.push(arg.into());
265        self
266    }
267
268    /// Add multiple handler arguments (advanced usage)
269    ///
270    /// # Platform Behavior
271    /// - **Desktop/Linux/Android**: Passed to handler process
272    /// - **iOS/tvOS/watchOS**: Ignored
273    ///
274    /// # Example
275    /// ```rust
276    /// # use crashpad_rs::CrashpadConfig;
277    /// let config = CrashpadConfig::builder()
278    ///     .handler_arguments(vec!["--monitor-self", "--no-rate-limit"])
279    ///     .build();
280    /// ```
281    pub fn handler_arguments<I, S>(mut self, args: I) -> Self
282    where
283        I: IntoIterator<Item = S>,
284        S: Into<String>,
285    {
286        self.config
287            .handler_arguments
288            .extend(args.into_iter().map(Into::into));
289        self
290    }
291
292    /// Build the configuration
293    pub fn build(self) -> CrashpadConfig {
294        self.config
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn test_builder() {
304        let config = CrashpadConfig::builder()
305            .handler_path("/usr/local/bin/crashpad_handler")
306            .database_path("/tmp/crashes")
307            .url("https://crashes.example.com")
308            .build();
309
310        assert_eq!(
311            config.handler_path.to_str().unwrap(),
312            "/usr/local/bin/crashpad_handler"
313        );
314        assert_eq!(config.database_path.to_str().unwrap(), "/tmp/crashes");
315        assert_eq!(config.url.as_deref(), Some("https://crashes.example.com"));
316    }
317
318    #[test]
319    #[cfg(not(any(target_os = "ios", target_os = "tvos", target_os = "watchos")))]
320    fn test_handler_path_fallback() {
321        // Test 1: Explicit path in config takes precedence
322        let config = CrashpadConfig::builder()
323            .handler_path("/explicit/path/crashpad_handler")
324            .build();
325
326        // Should return the explicit path even if it doesn't exist
327        assert_eq!(
328            config.handler_path().unwrap().to_str().unwrap(),
329            "/explicit/path/crashpad_handler"
330        );
331
332        // Test 2: Empty config should trigger fallback search
333        let config = CrashpadConfig::builder().build();
334
335        // This will search through fallbacks
336        // The actual result depends on environment
337        let result = config.handler_path();
338
339        // If it finds a handler, it should be one of the expected names
340        if let Ok(path) = result {
341            let filename = path.file_name().unwrap().to_str().unwrap();
342            assert!(
343                filename == "crashpad_handler"
344                    || filename == "crashpad_handler.exe"
345                    || filename == "libcrashpad_handler.so"
346            );
347        }
348    }
349
350    #[test]
351    #[cfg(not(any(target_os = "ios", target_os = "tvos", target_os = "watchos")))]
352    fn test_handler_env_var() {
353        // Test that CRASHPAD_HANDLER environment variable is checked
354        // Note: This test might interact with actual environment
355
356        // Save current env var if it exists
357        let original = env::var("CRASHPAD_HANDLER").ok();
358
359        // Set a test path
360        env::set_var("CRASHPAD_HANDLER", "/env/path/crashpad_handler");
361
362        let config = CrashpadConfig::builder().build();
363
364        // The handler_path method should check env var as fallback
365        // (actual behavior depends on whether file exists)
366        let _result = config.handler_path();
367
368        // Restore original env var
369        if let Some(orig) = original {
370            env::set_var("CRASHPAD_HANDLER", orig);
371        } else {
372            env::remove_var("CRASHPAD_HANDLER");
373        }
374    }
375
376    #[test]
377    fn test_handler_arguments_high_level() {
378        // Test high-level API methods
379        let config = CrashpadConfig::builder()
380            .rate_limit(false)
381            .upload_gzip(false)
382            .periodic_tasks(false)
383            .identify_client_via_url(false)
384            .build();
385
386        assert!(config
387            .handler_arguments
388            .contains(&"--no-rate-limit".to_string()));
389        assert!(config
390            .handler_arguments
391            .contains(&"--no-upload-gzip".to_string()));
392        assert!(config
393            .handler_arguments
394            .contains(&"--no-periodic-tasks".to_string()));
395        assert!(config
396            .handler_arguments
397            .contains(&"--no-identify-client-via-url".to_string()));
398    }
399
400    #[test]
401    fn test_handler_arguments_low_level() {
402        // Test low-level API methods
403        let config = CrashpadConfig::builder()
404            .handler_argument("--monitor-self")
405            .handler_arguments(vec!["--arg1", "--arg2"])
406            .build();
407
408        assert!(config
409            .handler_arguments
410            .contains(&"--monitor-self".to_string()));
411        assert!(config.handler_arguments.contains(&"--arg1".to_string()));
412        assert!(config.handler_arguments.contains(&"--arg2".to_string()));
413    }
414
415    #[test]
416    fn test_handler_arguments_mixed() {
417        // Test mixing high-level and low-level APIs
418        let config = CrashpadConfig::builder()
419            .rate_limit(false)
420            .handler_argument("--monitor-self")
421            .upload_gzip(true) // Enabled, should not add argument
422            .handler_argument("--monitor-self-annotation=test=value")
423            .build();
424
425        assert!(config
426            .handler_arguments
427            .contains(&"--no-rate-limit".to_string()));
428        assert!(config
429            .handler_arguments
430            .contains(&"--monitor-self".to_string()));
431        assert!(config
432            .handler_arguments
433            .contains(&"--monitor-self-annotation=test=value".to_string()));
434        // Should NOT contain --no-upload-gzip since upload_gzip(true)
435        assert!(!config
436            .handler_arguments
437            .contains(&"--no-upload-gzip".to_string()));
438    }
439
440    #[test]
441    fn test_handler_arguments_default() {
442        // Test that default config has no handler arguments
443        let config = CrashpadConfig::default();
444        assert!(config.handler_arguments.is_empty());
445    }
446}