Skip to main content

chaser_cf/core/
config.rs

1//! Configuration for chaser-cf
2
3use crate::models::Profile;
4use std::env;
5use std::path::PathBuf;
6use std::time::Duration;
7
8/// Configuration for ChaserCF
9#[derive(Debug, Clone)]
10pub struct ChaserConfig {
11    /// Maximum concurrent browser contexts (default: 20)
12    pub context_limit: usize,
13
14    /// Request timeout in milliseconds (default: 60000)
15    pub timeout_ms: u64,
16
17    /// Stealth profile to use (default: Windows)
18    pub profile: Profile,
19
20    /// Whether to defer browser initialization until first use (default: false)
21    pub lazy_init: bool,
22
23    /// Path to Chrome/Chromium binary (default: auto-detect)
24    pub chrome_path: Option<PathBuf>,
25
26    /// Whether to run in headless mode (default: false for stealth)
27    pub headless: bool,
28
29    /// Browser viewport width (default: 1920)
30    pub viewport_width: u32,
31
32    /// Browser viewport height (default: 1080)
33    pub viewport_height: u32,
34
35    /// Extra command-line flags to pass to the Chrome process, appended
36    /// to chaser-cf's defaults (`--disable-blink-features=Automation-
37    /// Controlled`, `--disable-infobars`).
38    ///
39    /// Common values:
40    /// - `--no-sandbox` — required when the host process runs as root
41    ///   (e.g. systemd unit, Docker containers without `--user`),
42    ///   otherwise Chrome refuses to start with the message
43    ///   "Running as root without --no-sandbox is not supported".
44    /// - `--disable-gpu` — for headless servers without a GPU.
45    /// - `--disable-dev-shm-usage` — for /dev/shm-constrained containers.
46    ///
47    /// Default: empty (chaser-cf only sets its own minimum baseline flags).
48    pub extra_args: Vec<String>,
49
50    /// Spin up a virtual X display (Xvfb) and run Chrome headed inside it.
51    /// Linux only — ignored on macOS/Windows. Requires `Xvfb` to be installed
52    /// (`apt install xvfb`). Overrides `headless`: Chrome always runs headed
53    /// when a virtual display is active. Default: false.
54    pub virtual_display: bool,
55}
56
57impl Default for ChaserConfig {
58    fn default() -> Self {
59        Self {
60            context_limit: 20,
61            timeout_ms: 60000,
62            profile: Profile::Windows,
63            lazy_init: false,
64            chrome_path: None,
65            headless: false,
66            viewport_width: 1920,
67            viewport_height: 1080,
68            extra_args: Vec::new(),
69            virtual_display: false,
70        }
71    }
72}
73
74impl ChaserConfig {
75    /// Create configuration from environment variables
76    ///
77    /// Environment variables:
78    /// - `CHASER_CONTEXT_LIMIT`: Max concurrent contexts (default: 20)
79    /// - `CHASER_TIMEOUT`: Timeout in ms (default: 60000)
80    /// - `CHASER_PROFILE`: Profile name (windows/linux/macos)
81    /// - `CHASER_LAZY_INIT`: Enable lazy init (true/false)
82    /// - `CHROME_BIN`: Path to Chrome binary
83    /// - `CHASER_HEADLESS`: Run headless (true/false)
84    /// - `CHASER_EXTRA_ARGS`: Whitespace-separated Chrome flags appended to
85    ///   chaser-cf's defaults (e.g. `--no-sandbox --disable-gpu`)
86    pub fn from_env() -> Self {
87        let mut config = Self::default();
88
89        if let Ok(val) = env::var("CHASER_CONTEXT_LIMIT") {
90            if let Ok(limit) = val.parse() {
91                config.context_limit = limit;
92            }
93        }
94
95        if let Ok(val) = env::var("CHASER_TIMEOUT") {
96            if let Ok(timeout) = val.parse() {
97                config.timeout_ms = timeout;
98            }
99        }
100
101        if let Ok(val) = env::var("CHASER_PROFILE") {
102            if let Some(profile) = Profile::parse(&val) {
103                config.profile = profile;
104            }
105        }
106
107        if let Ok(val) = env::var("CHASER_LAZY_INIT") {
108            config.lazy_init = val.eq_ignore_ascii_case("true") || val == "1";
109        }
110
111        if let Ok(val) = env::var("CHROME_BIN") {
112            config.chrome_path = Some(PathBuf::from(val));
113        }
114
115        if let Ok(val) = env::var("CHASER_HEADLESS") {
116            config.headless = val.eq_ignore_ascii_case("true") || val == "1";
117        }
118
119        if let Ok(val) = env::var("CHASER_EXTRA_ARGS") {
120            config.extra_args = val.split_whitespace().map(|s| s.to_string()).collect();
121        }
122
123        if let Ok(val) = env::var("CHASER_VIRTUAL_DISPLAY") {
124            config.virtual_display = val.eq_ignore_ascii_case("true") || val == "1";
125        }
126
127        config
128    }
129
130    /// Builder method: set context limit
131    pub fn with_context_limit(mut self, limit: usize) -> Self {
132        self.context_limit = limit;
133        self
134    }
135
136    /// Builder method: set timeout
137    pub fn with_timeout_ms(mut self, timeout: u64) -> Self {
138        self.timeout_ms = timeout;
139        self
140    }
141
142    /// Builder method: set timeout from Duration
143    pub fn with_timeout(mut self, timeout: Duration) -> Self {
144        self.timeout_ms = timeout.as_millis() as u64;
145        self
146    }
147
148    /// Builder method: set profile
149    pub fn with_profile(mut self, profile: Profile) -> Self {
150        self.profile = profile;
151        self
152    }
153
154    /// Builder method: enable lazy initialization
155    pub fn with_lazy_init(mut self, lazy: bool) -> Self {
156        self.lazy_init = lazy;
157        self
158    }
159
160    /// Builder method: set Chrome path
161    pub fn with_chrome_path(mut self, path: impl Into<PathBuf>) -> Self {
162        self.chrome_path = Some(path.into());
163        self
164    }
165
166    /// Builder method: set headless mode
167    pub fn with_headless(mut self, headless: bool) -> Self {
168        self.headless = headless;
169        self
170    }
171
172    /// Builder method: set viewport size
173    pub fn with_viewport(mut self, width: u32, height: u32) -> Self {
174        self.viewport_width = width;
175        self.viewport_height = height;
176        self
177    }
178
179    /// Builder method: replace the extra Chrome args set with the given list.
180    /// Use [`Self::add_extra_arg`] to append a single flag instead.
181    pub fn with_extra_args<I, S>(mut self, args: I) -> Self
182    where
183        I: IntoIterator<Item = S>,
184        S: Into<String>,
185    {
186        self.extra_args = args.into_iter().map(Into::into).collect();
187        self
188    }
189
190    /// Builder method: append a single Chrome flag to the existing extras.
191    /// Useful for chaining, e.g.
192    /// `ChaserConfig::default().add_extra_arg("--no-sandbox")`.
193    pub fn add_extra_arg(mut self, arg: impl Into<String>) -> Self {
194        self.extra_args.push(arg.into());
195        self
196    }
197
198    /// Builder method: enable virtual X display (Xvfb) for headed Chrome on Linux.
199    pub fn with_virtual_display(mut self, enabled: bool) -> Self {
200        self.virtual_display = enabled;
201        self
202    }
203
204    /// Get timeout as Duration
205    pub fn timeout(&self) -> Duration {
206        Duration::from_millis(self.timeout_ms)
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_default_config() {
216        let config = ChaserConfig::default();
217        assert_eq!(config.context_limit, 20);
218        assert_eq!(config.timeout_ms, 60000);
219        assert_eq!(config.profile, Profile::Windows);
220        assert!(!config.lazy_init);
221        assert!(!config.headless);
222        assert!(config.extra_args.is_empty());
223    }
224
225    #[test]
226    fn test_with_extra_args_replaces() {
227        let config = ChaserConfig::default().with_extra_args(["--no-sandbox", "--disable-gpu"]);
228        assert_eq!(config.extra_args, vec!["--no-sandbox", "--disable-gpu"]);
229        // with_extra_args replaces; calling again clears the previous set
230        let config2 = config.with_extra_args(vec!["--foo"]);
231        assert_eq!(config2.extra_args, vec!["--foo"]);
232    }
233
234    #[test]
235    fn test_add_extra_arg_appends() {
236        let config = ChaserConfig::default()
237            .add_extra_arg("--no-sandbox")
238            .add_extra_arg("--disable-gpu");
239        assert_eq!(config.extra_args, vec!["--no-sandbox", "--disable-gpu"]);
240    }
241
242    #[test]
243    fn test_builder_pattern() {
244        let config = ChaserConfig::default()
245            .with_context_limit(10)
246            .with_timeout_ms(30000)
247            .with_profile(Profile::Linux)
248            .with_lazy_init(true)
249            .with_headless(true);
250
251        assert_eq!(config.context_limit, 10);
252        assert_eq!(config.timeout_ms, 30000);
253        assert_eq!(config.profile, Profile::Linux);
254        assert!(config.lazy_init);
255        assert!(config.headless);
256    }
257}